blob: bb244b2993e9047ca1e2be0aea360337e8f95a5b [file] [log] [blame]
/*
* 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.server.sftp;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.Date;
import java.util.EnumSet;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.util.Buffer;
import org.apache.sshd.common.util.IoUtils;
import org.apache.sshd.common.util.SelectorUtils;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
import org.apache.sshd.common.file.FileSystemAware;
import org.apache.sshd.common.file.FileSystemView;
import org.apache.sshd.server.SessionAware;
import org.apache.sshd.common.file.SshFile;
import org.apache.sshd.server.session.ServerSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* SFTP subsystem
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class SftpSubsystem implements Command, Runnable, SessionAware, FileSystemAware {
protected final Logger log = LoggerFactory.getLogger(getClass());
public static class Factory implements NamedFactory<Command> {
public Factory() {
}
public Command create() {
return new SftpSubsystem();
}
public String getName() {
return "sftp";
}
}
/**
* Properties key for the maximum of available open handles per session.
*/
public static final String MAX_OPEN_HANDLES_PER_SESSION = "max-open-handles-per-session";
public static final int LOWER_SFTP_IMPL = 3; // Working implementation from v3
public static final int HIGHER_SFTP_IMPL = 3; // .. up to
public static final String ALL_SFTP_IMPL = "3";
public static final int MAX_PACKET_LENGTH = 1024 * 16;
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;
public static final int S_IFMT = 0170000; // bitmask for the file type bitfields
public static final int S_IFSOCK = 0140000; // socket
public static final int S_IFLNK = 0120000; // symbolic link
public static final int S_IFREG = 0100000; // regular file
public static final int S_IFBLK = 0060000; // block device
public static final int S_IFDIR = 0040000; // directory
public static final int S_IFCHR = 0020000; // character device
public static final int S_IFIFO = 0010000; // fifo
public static final int S_ISUID = 0004000; // set UID bit
public static final int S_ISGID = 0002000; // set GID bit
public static final int S_ISVTX = 0001000; // sticky bit
public static final int S_IRUSR = 0000400;
public static final int S_IWUSR = 0000200;
public static final int S_IXUSR = 0000100;
public static final int S_IRGRP = 0000040;
public static final int S_IWGRP = 0000020;
public static final int S_IXGRP = 0000010;
public static final int S_IROTH = 0000004;
public static final int S_IWOTH = 0000002;
public static final int S_IXOTH = 0000001;
private ExitCallback callback;
private InputStream in;
private OutputStream out;
private OutputStream err;
private Environment env;
private ServerSession session;
private boolean closed = false;
private FileSystemView root;
private int version;
private Map<String, Handle> handles = new HashMap<String, Handle>();
protected static abstract class Handle {
SshFile file;
public Handle(SshFile file) {
this.file = file;
}
public SshFile getFile() {
return file;
}
public void close() throws IOException {
file.handleClose();
}
}
protected static class DirectoryHandle extends Handle implements Iterator<SshFile> {
boolean done;
// the directory should be read once at "open directory"
List<SshFile> fileList = null;
int fileIndex;
public DirectoryHandle(SshFile file) {
super(file);
fileList = file.listSshFiles();
fileIndex = 0;
}
public boolean isDone() {
return done;
}
public void setDone(boolean done) {
this.done = done;
}
public boolean hasNext() {
return fileIndex < fileList.size();
}
public SshFile next() {
SshFile f = fileList.get(fileIndex);
fileIndex++;
return f;
}
public void remove() {
throw new UnsupportedOperationException();
}
public void clearFileList() {
// allow the garbage collector to do the job
fileList = null;
}
}
protected static class FileHandle extends Handle {
OutputStream output;
long outputPos;
InputStream input;
long inputPos;
long length;
public FileHandle(SshFile sshFile) {
super(sshFile);
}
public int read(byte[] data, long offset) throws IOException {
if (input != null && offset >= length) {
return -1;
}
if (input != null && offset != inputPos) {
IoUtils.closeQuietly(input);
input = null;
}
if (input == null) {
input = file.createInputStream(offset);
length = file.getSize();
inputPos = offset;
}
if (offset >= length) {
return -1;
}
int read = input.read(data);
inputPos += read;
return read;
}
public void write(byte[] data, long offset) throws IOException {
if (output != null && offset != outputPos) {
IoUtils.closeQuietly(output);
output = null;
}
if (output == null) {
output = file.createOutputStream(offset);
}
output.write(data);
outputPos += data.length;
}
@Override
public void close() throws IOException {
IoUtils.closeQuietly(output, input);
output = null;
input = null;
super.close();
}
}
public SftpSubsystem() {}
public void setSession(ServerSession session) {
this.session = session;
}
public void setFileSystemView(FileSystemView view) {
this.root = view;
}
public void setExitCallback(ExitCallback callback) {
this.callback = callback;
}
public void setInputStream(InputStream in) {
this.in = in;
}
public void setOutputStream(OutputStream out) {
this.out = out;
}
public void setErrorStream(OutputStream err) {
this.err = err;
}
public void start(Environment env) throws IOException {
this.env = env;
new Thread(this).start();
}
public void run() {
DataInputStream dis = null;
try {
dis = new DataInputStream(in);
while (true) {
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;
}
process(buffer);
}
} catch (Throwable t) {
if (!closed && !(t instanceof EOFException)) { // Ignore han
log.error("Exception caught in SFTP subsystem", t);
}
} finally {
if (dis != null) {
try {
dis.close();
} catch (IOException ioe) {
log.error("Could not close DataInputStream", ioe);
}
}
if (handles != null) {
for (Map.Entry<String, Handle> entry : handles.entrySet()) {
Handle handle = entry.getValue();
try {
handle.close();
} catch (IOException ioe) {
log.error("Could not close open handle: " + entry.getKey(), ioe);
}
}
}
dis = null;
callback.onExit(0);
}
}
protected void process(Buffer buffer) throws IOException {
int length = buffer.getInt();
int type = buffer.getByte();
int id = buffer.getInt();
switch (type) {
case SSH_FXP_INIT: {
log.debug("Received SSH_FXP_INIT (version={})", version);
if (length != 5) {
throw new IllegalArgumentException();
}
version = id;
if (version >= LOWER_SFTP_IMPL) {
version = Math.min(version, HIGHER_SFTP_IMPL);
buffer.clear();
buffer.putByte((byte) SSH_FXP_VERSION);
buffer.putInt(version);
send(buffer);
} else {
// We only support version 3 (Version 1 and 2 are not common)
sendStatus(id, SSH_FX_OP_UNSUPPORTED, "SFTP server only support versions " + ALL_SFTP_IMPL);
}
break;
}
case SSH_FXP_OPEN: {
if (session.getFactoryManager().getProperties() != null) {
String maxHandlesString = session.getFactoryManager().getProperties().get(MAX_OPEN_HANDLES_PER_SESSION);
if (maxHandlesString != null) {
int maxHandleCount = Integer.parseInt(maxHandlesString);
if (handles.size() > maxHandleCount) {
sendStatus(id, SSH_FX_FAILURE, "Too many open handles");
break;
}
}
}
String path = buffer.getString();
int pflags = buffer.getInt();
Map<SshFile.Attribute, Object> attrs = readAttrs(buffer);
log.debug("Received SSH_FXP_OPEN (path={}, pflags={}, attrs={})", new Object[] { path, pflags, attrs });
try {
SshFile file = resolveFile(path);
if (file.doesExist()) {
if (((pflags & SSH_FXF_CREAT) != 0) && ((pflags & SSH_FXF_EXCL) != 0)) {
sendStatus(id, SSH_FX_FAILURE, path);
return;
}
} else {
if (((pflags & SSH_FXF_CREAT) != 0)) {
if (!file.isWritable()) {
sendStatus(id, SSH_FX_PERMISSION_DENIED, "Can not create " + path);
return;
}
file.create();
}
}
String acc = ((pflags & (SSH_FXF_READ | SSH_FXF_WRITE)) != 0 ? "r" : "") +
((pflags & SSH_FXF_WRITE) != 0 ? "w" : "");
if ((pflags & SSH_FXF_TRUNC) != 0) {
file.truncate();
}
if ((pflags & SSH_FXF_CREAT) != 0) {
file.setAttributes(attrs);
}
String handle = UUID.randomUUID().toString();
handles.put(handle, new FileHandle(file));
sendHandle(id, handle);
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage() == null ? "" : e.getMessage());
}
break;
}
case SSH_FXP_CLOSE: {
String handle = buffer.getString();
log.debug("Received SSH_FXP_CLOSE (handle={})", handle);
try {
Handle h = handles.get(handle);
if (h == null) {
sendStatus(id, SSH_FX_FAILURE, handle, "");
} else {
handles.remove(handle);
h.close();
sendStatus(id, SSH_FX_OK, "", "");
}
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_READ: {
String handle = buffer.getString();
long offset = buffer.getLong();
int len = buffer.getInt();
log.debug("Received SSH_FXP_READ (handle={}, offset={}, length={})", new Object[] { handle, offset, len });
try {
Handle p = handles.get(handle);
if (!(p instanceof FileHandle)) {
sendStatus(id, SSH_FX_FAILURE, handle);
} else {
FileHandle fh = (FileHandle) p;
byte[] b = new byte[Math.min(len, Buffer.MAX_LEN)];
len = fh.read(b, offset);
if (len >= 0) {
Buffer buf = new Buffer(len + 5);
buf.putByte((byte) SSH_FXP_DATA);
buf.putInt(id);
buf.putBytes(b, 0, len);
send(buf);
} else {
sendStatus(id, SSH_FX_EOF, "");
}
}
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_WRITE: {
String handle = buffer.getString();
long offset = buffer.getLong();
byte[] data = buffer.getBytes();
log.debug("Received SSH_FXP_WRITE (handle={}, offset={}, data=byte[{}])", new Object[] { handle, offset, data.length });
try {
Handle p = handles.get(handle);
if (!(p instanceof FileHandle)) {
sendStatus(id, SSH_FX_FAILURE, handle);
} else {
FileHandle fh = (FileHandle) p;
fh.write(data, offset);
SshFile sshFile = fh.getFile();
sshFile.setLastModified(new Date().getTime());
sendStatus(id, SSH_FX_OK, "");
}
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_LSTAT: {
String path = buffer.getString();
log.debug("Received SSH_FXP_LSTAT (path={})", path);
try {
SshFile p = resolveFile(path);
sendAttrs(id, p, false);
} catch (FileNotFoundException e) {
sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_FSTAT: {
String handle = buffer.getString();
log.debug("Received SSH_FXP_FSTAT (handle={})", handle);
try {
Handle p = handles.get(handle);
if (p == null) {
sendStatus(id, SSH_FX_FAILURE, handle);
} else {
sendAttrs(id, p.getFile(), true);
}
} catch (FileNotFoundException e) {
sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_SETSTAT: {
String path = buffer.getString();
Map<SshFile.Attribute, Object> attrs = readAttrs(buffer);
log.debug("Received SSH_FXP_SETSTAT (path={}, attrs={})", path, attrs);
try {
SshFile p = resolveFile(path);
p.setAttributes(attrs);
sendStatus(id, SSH_FX_OK, "");
} catch (FileNotFoundException e) {
sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
} catch (UnsupportedOperationException e) {
sendStatus(id, SSH_FX_FAILURE, "");
}
break;
}
case SSH_FXP_FSETSTAT: {
String handle = buffer.getString();
Map<SshFile.Attribute, Object> attrs = readAttrs(buffer);
log.debug("Received SSH_FXP_FSETSTAT (handle={}, attrs={})", handle, attrs);
try {
Handle p = handles.get(handle);
if (p == null) {
sendStatus(id, SSH_FX_FAILURE, handle);
} else {
p.getFile().setAttributes(attrs);
sendStatus(id, SSH_FX_OK, "");
}
} catch (FileNotFoundException e) {
sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
} catch (UnsupportedOperationException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_OPENDIR: {
String path = buffer.getString();
log.debug("Received SSH_FXP_OPENDIR (path={})", path);
try {
SshFile p = resolveFile(path);
if (!p.doesExist()) {
sendStatus(id, SSH_FX_NO_SUCH_FILE, path);
} else if (!p.isDirectory()) {
sendStatus(id, SSH_FX_NO_SUCH_FILE, path);
} else if (!p.isReadable()) {
sendStatus(id, SSH_FX_PERMISSION_DENIED, path);
} else {
String handle = UUID.randomUUID().toString();
handles.put(handle, new DirectoryHandle(p));
sendHandle(id, handle);
}
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_READDIR: {
String handle = buffer.getString();
log.debug("Received SSH_FXP_READDIR (handle={})", handle);
try {
Handle p = handles.get(handle);
if (!(p instanceof DirectoryHandle)) {
sendStatus(id, SSH_FX_FAILURE, handle);
} else if (((DirectoryHandle) p).isDone()) {
sendStatus(id, SSH_FX_EOF, "", "");
} else if (!p.getFile().doesExist()) {
sendStatus(id, SSH_FX_NO_SUCH_FILE, p.getFile().getAbsolutePath());
} else if (!p.getFile().isDirectory()) {
sendStatus(id, SSH_FX_NO_SUCH_FILE, p.getFile().getAbsolutePath());
} else if (!p.getFile().isReadable()) {
sendStatus(id, SSH_FX_PERMISSION_DENIED, p.getFile().getAbsolutePath());
} else {
DirectoryHandle dh = (DirectoryHandle) p;
if (dh.hasNext()) {
// There is at least one file in the directory.
// Send only a few files at a time to not create packets of a too
// large size or have a timeout to occur.
sendName(id, dh);
if (!dh.hasNext()) {
// if no more files to send
dh.setDone(true);
dh.clearFileList();
}
} else {
// empty directory
dh.setDone(true);
dh.clearFileList();
sendStatus(id, SSH_FX_EOF, "", "");
}
}
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_REMOVE: {
String path = buffer.getString();
log.debug("Received SSH_FXP_REMOVE (path={})", path);
try {
SshFile p = resolveFile(path);
if (!p.doesExist()) {
sendStatus(id, SSH_FX_NO_SUCH_FILE, p.getAbsolutePath());
} else if (p.isDirectory()) {
sendStatus(id, SSH_FX_NO_SUCH_FILE, p.getAbsolutePath());
} else if (!p.delete()) {
sendStatus(id, SSH_FX_FAILURE, "Failed to delete file");
} else {
sendStatus(id, SSH_FX_OK, "");
}
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_MKDIR: {
String path = buffer.getString();
Map<SshFile.Attribute, Object> attrs = readAttrs(buffer);
log.debug("Received SSH_FXP_MKDIR (path={})", path);
// attrs
try {
SshFile p = resolveFile(path);
if (p.doesExist()) {
if (p.isDirectory()) {
sendStatus(id, SSH_FX_FAILURE, p.getAbsolutePath());
} else {
sendStatus(id, SSH_FX_NO_SUCH_FILE, p.getAbsolutePath());
}
} else if (!p.isWritable()) {
sendStatus(id, SSH_FX_PERMISSION_DENIED, p.getAbsolutePath());
} else if (!p.mkdir()) {
throw new IOException("Error creating dir " + path);
} else {
p.setAttributes(attrs);
sendStatus(id, SSH_FX_OK, "");
}
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_RMDIR: {
String path = buffer.getString();
log.debug("Received SSH_FXP_RMDIR (path={})", path);
// attrs
try {
SshFile p = resolveFile(path);
if (p.isDirectory()) {
if (p.doesExist()) {
if (p.listSshFiles().size() == 0) {
if (p.delete()) {
sendStatus(id, SSH_FX_OK, "");
} else {
sendStatus(id, SSH_FX_FAILURE, "Unable to delete directory " + path);
}
} else {
sendStatus(id, SSH_FX_FAILURE, path);
}
} else {
sendStatus(id, SSH_FX_NO_SUCH_FILE, path);
}
} else {
sendStatus(id, SSH_FX_NO_SUCH_FILE, p.getAbsolutePath());
}
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_REALPATH: {
String path = buffer.getString();
log.debug("Received SSH_FXP_REALPATH (path={})", path);
if (path.trim().length() == 0) {
path = ".";
}
try {
SshFile p = resolveFile(path);
sendPath(id, p, false);
} catch (FileNotFoundException e) {
e.printStackTrace();
sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
} catch (IOException e) {
e.printStackTrace();
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_STAT: {
String path = buffer.getString();
log.debug("Received SSH_FXP_STAT (path={})", path);
try {
SshFile p = resolveFile(path);
sendAttrs(id, p, true);
} catch (FileNotFoundException e) {
sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_RENAME: {
String oldPath = buffer.getString();
String newPath = buffer.getString();
log.debug("Received SSH_FXP_RENAME (oldPath={}, newPath={})", oldPath, newPath);
try {
SshFile o = resolveFile(oldPath);
SshFile n = resolveFile(newPath);
if (!o.doesExist()) {
sendStatus(id, SSH_FX_NO_SUCH_FILE, o.getAbsolutePath());
} else if (n.doesExist()) {
sendStatus(id, SSH_FX_FAILURE, n.getAbsolutePath());
} else if (!o.move(n)) {
sendStatus(id, SSH_FX_FAILURE, "Failed to rename file");
} else {
sendStatus(id, SSH_FX_OK, "");
}
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_READLINK: {
String path = buffer.getString();
log.debug("Received SSH_FXP_READLINK (path={})", path);
try {
SshFile f = resolveFile(path);
String l = f.readSymbolicLink();
sendLink(id, l);
} catch (UnsupportedOperationException e) {
sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command " + type + " is unsupported or not implemented");
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
case SSH_FXP_SYMLINK: {
String linkpath = buffer.getString();
String targetpath = buffer.getString();
log.debug("Received SSH_FXP_SYMLINK (linkpath={}, targetpath={})", linkpath, targetpath);
try {
SshFile link = resolveFile(linkpath);
SshFile target = resolveFile(targetpath);
link.createSymbolicLink(target);
sendStatus(id, SSH_FX_OK, "");
} catch (UnsupportedOperationException e) {
sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command " + type + " is unsupported or not implemented");
} catch (IOException e) {
sendStatus(id, SSH_FX_FAILURE, e.getMessage());
}
break;
}
default: {
log.error("Received: {}", type);
sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command " + type + " is unsupported or not implemented");
break;
}
}
}
protected void sendHandle(int id, String handle) throws IOException {
Buffer buffer = new Buffer();
buffer.putByte((byte) SSH_FXP_HANDLE);
buffer.putInt(id);
buffer.putString(handle);
send(buffer);
}
protected void sendAttrs(int id, SshFile file, boolean followLinks) throws IOException {
Buffer buffer = new Buffer();
buffer.putByte((byte) SSH_FXP_ATTRS);
buffer.putInt(id);
writeAttrs(buffer, file, followLinks);
send(buffer);
}
protected void sendPath(int id, SshFile f) throws IOException {
sendPath(id, f, true);
}
protected void sendPath(int id, SshFile f, boolean sendAttrs) throws IOException {
Buffer buffer = new Buffer();
buffer.putByte((byte) SSH_FXP_NAME);
buffer.putInt(id);
buffer.putInt(1);
//normalize the given path, use *nix style separator
String normalizedPath = SelectorUtils.normalizePath(f.getAbsolutePath(), "/");
if (normalizedPath.length() == 0) {
normalizedPath = "/";
}
buffer.putString(normalizedPath);
f = resolveFile(normalizedPath);
if (f.getName().length() == 0) {
f = resolveFile(".");
}
buffer.putString(getLongName(f, sendAttrs)); // Format specified in the specs
buffer.putInt(0);
send(buffer);
}
protected void sendLink(int id, String link) throws IOException {
Buffer buffer = new Buffer();
buffer.putByte((byte) SSH_FXP_NAME);
buffer.putInt(id);
buffer.putInt(1);
//normalize the given path, use *nix style separator
buffer.putString(link);
buffer.putString(link);
buffer.putInt(0);
send(buffer);
}
protected void sendName(int id, Iterator<SshFile> files) throws IOException {
Buffer buffer = new Buffer();
buffer.putByte((byte) SSH_FXP_NAME);
buffer.putInt(id);
int wpos = buffer.wpos();
buffer.putInt(0);
int nb = 0;
while (files.hasNext() && buffer.wpos() < MAX_PACKET_LENGTH) {
SshFile f = files.next();
buffer.putString(f.getName());
buffer.putString(getLongName(f)); // Format specified in the specs
writeAttrs(buffer, f, false);
nb++;
}
int oldpos = buffer.wpos();
buffer.wpos(wpos);
buffer.putInt(nb);
buffer.wpos(oldpos);
send(buffer);
}
private String getLongName(SshFile f) throws IOException {
return getLongName(f, true);
}
private String getLongName(SshFile f, boolean sendAttrs) throws IOException {
Map<SshFile.Attribute, Object> attributes;
if (sendAttrs) {
attributes = f.getAttributes(true);
} else {
attributes = new HashMap<SshFile.Attribute, Object>();
attributes.put(SshFile.Attribute.Owner, "owner");
attributes.put(SshFile.Attribute.Group, "group");
attributes.put(SshFile.Attribute.Size, (long) 0);
attributes.put(SshFile.Attribute.IsDirectory, false);
attributes.put(SshFile.Attribute.IsSymbolicLink, false);
attributes.put(SshFile.Attribute.IsRegularFile, false);
attributes.put(SshFile.Attribute.Permissions, EnumSet.noneOf(SshFile.Permission.class));
attributes.put(SshFile.Attribute.LastModifiedTime, (long) 0);
}
String username = (String) attributes.get(SshFile.Attribute.Owner);
if (username.length() > 8) {
username = username.substring(0, 8);
} else {
for (int i = username.length(); i < 8; i++) {
username = username + " ";
}
}
String group = (String) attributes.get(SshFile.Attribute.Group);
if (group.length() > 8) {
group = group.substring(0, 8);
} else {
for (int i = group.length(); i < 8; i++) {
group = group + " ";
}
}
long length = (Long) attributes.get(SshFile.Attribute.Size);
String lengthString = String.format("%1$8s", length);
boolean isDirectory = (Boolean) attributes.get(SshFile.Attribute.IsDirectory);
boolean isLink = (Boolean) attributes.get(SshFile.Attribute.IsSymbolicLink);
int perms = getPermissions(attributes);
StringBuilder sb = new StringBuilder();
sb.append(isDirectory ? "d" : isLink ? "l" : "-");
sb.append((perms & S_IRUSR) != 0 ? "r" : "-");
sb.append((perms & S_IWUSR) != 0 ? "w" : "-");
sb.append((perms & S_IXUSR) != 0 ? "x" : "-");
sb.append((perms & S_IRGRP) != 0 ? "r" : "-");
sb.append((perms & S_IWGRP) != 0 ? "w" : "-");
sb.append((perms & S_IXGRP) != 0 ? "x" : "-");
sb.append((perms & S_IROTH) != 0 ? "r" : "-");
sb.append((perms & S_IWOTH) != 0 ? "w" : "-");
sb.append((perms & S_IXOTH) != 0 ? "x" : "-");
sb.append(" ");
sb.append(" 1");
sb.append(" ");
sb.append(username);
sb.append(" ");
sb.append(group);
sb.append(" ");
sb.append(lengthString);
sb.append(" ");
sb.append(getUnixDate((Long) attributes.get(SshFile.Attribute.LastModifiedTime)));
sb.append(" ");
sb.append(f.getName());
return sb.toString();
}
protected Map<SshFile.Attribute, Object> getPermissions(int perms) {
Map<SshFile.Attribute, Object> attrs = new HashMap<SshFile.Attribute, Object>();
if ((perms & S_IFMT) == S_IFREG) {
attrs.put(SshFile.Attribute.IsRegularFile, Boolean.TRUE);
}
if ((perms & S_IFMT) == S_IFDIR) {
attrs.put(SshFile.Attribute.IsDirectory, Boolean.TRUE);
}
if ((perms & S_IFMT) == S_IFLNK) {
attrs.put(SshFile.Attribute.IsSymbolicLink, Boolean.TRUE);
}
EnumSet<SshFile.Permission> p = EnumSet.noneOf(SshFile.Permission.class);
if ((perms & S_IRUSR) != 0) {
p.add(SshFile.Permission.UserRead);
}
if ((perms & S_IWUSR) != 0) {
p.add(SshFile.Permission.UserWrite);
}
if ((perms & S_IXUSR) != 0) {
p.add(SshFile.Permission.UserExecute);
}
if ((perms & S_IRGRP) != 0) {
p.add(SshFile.Permission.GroupRead);
}
if ((perms & S_IWGRP) != 0) {
p.add(SshFile.Permission.GroupWrite);
}
if ((perms & S_IXGRP) != 0) {
p.add(SshFile.Permission.GroupExecute);
}
if ((perms & S_IROTH) != 0) {
p.add(SshFile.Permission.OthersRead);
}
if ((perms & S_IWOTH) != 0) {
p.add(SshFile.Permission.OthersWrite);
}
if ((perms & S_IXOTH) != 0) {
p.add(SshFile.Permission.OthersExecute);
}
attrs.put(SshFile.Attribute.Permissions, p);
return attrs;
}
protected int getPermissions(Map<SshFile.Attribute, Object> attributes) {
boolean isReg = (Boolean) attributes.get(SshFile.Attribute.IsRegularFile);
boolean isDir = (Boolean) attributes.get(SshFile.Attribute.IsDirectory);
boolean isLnk = (Boolean) attributes.get(SshFile.Attribute.IsSymbolicLink);
int pf = 0;
EnumSet<SshFile.Permission> perms = (EnumSet<SshFile.Permission>) attributes.get(SshFile.Attribute.Permissions);
for (SshFile.Permission p : perms) {
switch (p) {
case UserRead: pf |= S_IRUSR; break;
case UserWrite: pf |= S_IWUSR; break;
case UserExecute: pf |= S_IXUSR; break;
case GroupRead: pf |= S_IRGRP; break;
case GroupWrite: pf |= S_IWGRP; break;
case GroupExecute: pf |= S_IXGRP; break;
case OthersRead: pf |= S_IROTH; break;
case OthersWrite: pf |= S_IWOTH; break;
case OthersExecute: pf |= S_IXOTH; break;
}
}
pf |= isReg ? S_IFREG : 0;
pf |= isDir ? S_IFDIR : 0;
pf |= isLnk ? S_IFLNK : 0;
return pf;
}
protected void writeAttrs(Buffer buffer, SshFile file, boolean followLinks) throws IOException {
if (!file.doesExist()) {
throw new FileNotFoundException(file.getAbsolutePath());
}
Map<SshFile.Attribute, Object> attributes = file.getAttributes(followLinks);
boolean isReg = getBool((Boolean) attributes.get(SshFile.Attribute.IsRegularFile));
boolean isDir = getBool((Boolean) attributes.get(SshFile.Attribute.IsDirectory));
boolean isLnk = getBool((Boolean) attributes.get(SshFile.Attribute.IsSymbolicLink));
int flags = 0;
if ((isReg || isLnk) && attributes.containsKey(SshFile.Attribute.Size)) {
flags |= SSH_FILEXFER_ATTR_SIZE;
}
if (attributes.containsKey(SshFile.Attribute.Uid) && attributes.containsKey(SshFile.Attribute.Gid)) {
flags |= SSH_FILEXFER_ATTR_UIDGID;
}
if (attributes.containsKey(SshFile.Attribute.Permissions)) {
flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
}
if (attributes.containsKey(SshFile.Attribute.LastAccessTime) && attributes.containsKey(SshFile.Attribute.LastModifiedTime)) {
flags |= SSH_FILEXFER_ATTR_ACMODTIME;
}
buffer.putInt(flags);
if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
buffer.putLong((Long) attributes.get(SshFile.Attribute.Size));
}
if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
buffer.putInt((Integer) attributes.get(SshFile.Attribute.Uid));
buffer.putInt((Integer) attributes.get(SshFile.Attribute.Gid));
}
if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
buffer.putInt(getPermissions(attributes));
}
if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
buffer.putInt(((Long) attributes.get(SshFile.Attribute.LastAccessTime)) / 1000);
buffer.putInt(((Long) attributes.get(SshFile.Attribute.LastModifiedTime)) / 1000);
}
}
protected boolean getBool(Boolean bool) {
return bool != null && bool;
}
protected Map<SshFile.Attribute, Object> readAttrs(Buffer buffer) throws IOException {
Map<SshFile.Attribute, Object> attrs = new HashMap<SshFile.Attribute, Object>();
int flags = buffer.getInt();
if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
attrs.put(SshFile.Attribute.Size, buffer.getLong());
}
if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
attrs.put(SshFile.Attribute.Uid, buffer.getInt());
attrs.put(SshFile.Attribute.Gid, buffer.getInt());
}
if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
attrs.putAll(getPermissions(buffer.getInt()));
}
if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
attrs.put(SshFile.Attribute.LastAccessTime, ((long) buffer.getInt()) * 1000);
attrs.put(SshFile.Attribute.LastModifiedTime, ((long) buffer.getInt()) * 1000);
}
return attrs;
}
protected void sendStatus(int id, int substatus, String msg) throws IOException {
sendStatus(id, substatus, msg, "");
}
protected void sendStatus(int id, int substatus, String msg, String lang) throws IOException {
log.debug("Send SSH_FXP_STATUS (substatus={}, msg={})", substatus, msg);
Buffer buffer = new Buffer();
buffer.putByte((byte) SSH_FXP_STATUS);
buffer.putInt(id);
buffer.putInt(substatus);
buffer.putString(msg);
buffer.putString(lang);
send(buffer);
}
protected void send(Buffer buffer) throws IOException {
DataOutputStream dos = new DataOutputStream(out);
dos.writeInt(buffer.available());
dos.write(buffer.array(), buffer.rpos(), buffer.available());
dos.flush();
}
public void destroy() {
closed = true;
}
private SshFile resolveFile(String path) {
return this.root.getNormalizedView().getFile(path);
}
private final static String[] MONTHS = { "Jan", "Feb", "Mar", "Apr", "May",
"Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
/**
* Get unix style date string.
*/
private final static String getUnixDate(long millis) {
if (millis < 0) {
return "------------";
}
StringBuffer sb = new StringBuffer(16);
Calendar cal = new GregorianCalendar();
cal.setTimeInMillis(millis);
// month
sb.append(MONTHS[cal.get(Calendar.MONTH)]);
sb.append(' ');
// day
int day = cal.get(Calendar.DATE);
if (day < 10) {
sb.append(' ');
}
sb.append(day);
sb.append(' ');
long sixMonth = 15811200000L; // 183L * 24L * 60L * 60L * 1000L;
long nowTime = System.currentTimeMillis();
if (Math.abs(nowTime - millis) > sixMonth) {
// year
int year = cal.get(Calendar.YEAR);
sb.append(' ');
sb.append(year);
} else {
// hour
int hh = cal.get(Calendar.HOUR_OF_DAY);
if (hh < 10) {
sb.append('0');
}
sb.append(hh);
sb.append(':');
// minute
int mm = cal.get(Calendar.MINUTE);
if (mm < 10) {
sb.append('0');
}
sb.append(mm);
}
return sb.toString();
}
}