org.apache.sshd.server.sftp.SftpSubsystem.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.sshd.server.sftp.SftpSubsystem.java

Source

/*
 * 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.*;
import java.util.*;

import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.util.Buffer;
import org.apache.sshd.common.util.LogUtils;
import org.apache.sshd.common.util.SelectorUtils;
import org.apache.sshd.server.*;
import org.apache.sshd.server.FileSystemView;
import org.apache.sshd.server.SshFile;
import org.apache.sshd.server.session.ServerSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * 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 Log log = LogFactory.getLog(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 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_LINK = 21;
    public static final int SSH_FXP_BLOCK = 22;
    public static final int SSH_FXP_UNBLOCK = 23;

    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_FX_INVALID_HANDLE = 9;
    public static final int SSH_FX_NO_SUCH_PATH = 10;
    public static final int SSH_FX_FILE_ALREADY_EXISTS = 11;
    public static final int SSH_FX_WRITE_PROTECT = 12;
    public static final int SSH_FX_NO_MEDIA = 13;
    public static final int SSH_FX_NO_SPACE_ON_FILESYSTEM = 14;
    public static final int SSH_FX_QUOTA_EXCEEDED = 15;
    public static final int SSH_FX_UNKNOWN_PRINCIPAL = 16;
    public static final int SSH_FX_LOCK_CONFLICT = 17;
    public static final int SSH_FX_DIR_NOT_EMPTY = 18;
    public static final int SSH_FX_NOT_A_DIRECTORY = 19;
    public static final int SSH_FX_INVALID_FILENAME = 20;
    public static final int SSH_FX_LINK_LOOP = 21;
    public static final int SSH_FX_CANNOT_DELETE = 22;
    public static final int SSH_FX_INVALID_PARAMETER = 23;
    public static final int SSH_FX_FILE_IS_A_DIRECTORY = 24;
    public static final int SSH_FX_BYTE_RANGE_LOCK_CONFLICT = 25;
    public static final int SSH_FX_BYTE_RANGE_LOCK_REFUSED = 26;
    public static final int SSH_FX_DELETE_PENDING = 27;
    public static final int SSH_FX_FILE_CORRUPT = 28;
    public static final int SSH_FX_OWNER_INVALID = 29;
    public static final int SSH_FX_GROUP_INVALID = 30;
    public static final int SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK = 31;

    public static final int SSH_FILEXFER_ATTR_SIZE = 0x00000001;
    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_ACCESSTIME = 0x00000008;
    public static final int SSH_FILEXFER_ATTR_CREATETIME = 0x00000010;
    public static final int SSH_FILEXFER_ATTR_MODIFYTIME = 0x00000020;
    public static final int SSH_FILEXFER_ATTR_ACL = 0x00000040;
    public static final int SSH_FILEXFER_ATTR_OWNERGROUP = 0x00000080;
    public static final int SSH_FILEXFER_ATTR_SUBSECOND_TIMES = 0x00000100;
    public static final int SSH_FILEXFER_ATTR_BITS = 0x00000200;
    public static final int SSH_FILEXFER_ATTR_ALLOCATION_SIZE = 0x00000400;
    public static final int SSH_FILEXFER_ATTR_TEXT_HINT = 0x00000800;
    public static final int SSH_FILEXFER_ATTR_MIME_TYPE = 0x00001000;
    public static final int SSH_FILEXFER_ATTR_LINK_COUNT = 0x00002000;
    public static final int SSH_FILEXFER_ATTR_UNTRANSLATED_NAME = 0x00004000;
    public static final int SSH_FILEXFER_ATTR_CTIME = 0x00008000;
    public static final int SSH_FILEXFER_ATTR_EXTENDED = 0x80000000;

    public static final int SSH_FILEXFER_TYPE_REGULAR = 1;
    public static final int SSH_FILEXFER_TYPE_DIRECTORY = 2;
    public static final int SSH_FILEXFER_TYPE_SYMLINK = 3;
    public static final int SSH_FILEXFER_TYPE_SPECIAL = 4;
    public static final int SSH_FILEXFER_TYPE_UNKNOWN = 5;
    public static final int SSH_FILEXFER_TYPE_SOCKET = 6;
    public static final int SSH_FILEXFER_TYPE_CHAR_DEVICE = 7;
    public static final int SSH_FILEXFER_TYPE_BLOCK_DEVICE = 8;
    public static final int SSH_FILEXFER_TYPE_FIFO = 9;

    public static final int SSH_FXF_ACCESS_DISPOSITION = 0x00000007;
    public static final int SSH_FXF_CREATE_NEW = 0x00000000;
    public static final int SSH_FXF_CREATE_TRUNCATE = 0x00000001;
    public static final int SSH_FXF_OPEN_EXISTING = 0x00000002;
    public static final int SSH_FXF_OPEN_OR_CREATE = 0x00000003;
    public static final int SSH_FXF_TRUNCATE_EXISTING = 0x00000004;
    public static final int SSH_FXF_APPEND_DATA = 0x00000008;
    public static final int SSH_FXF_APPEND_DATA_ATOMIC = 0x00000010;
    public static final int SSH_FXF_TEXT_MODE = 0x00000020;
    public static final int SSH_FXF_BLOCK_READ = 0x00000040;
    public static final int SSH_FXF_BLOCK_WRITE = 0x00000080;
    public static final int SSH_FXF_BLOCK_DELETE = 0x00000100;
    public static final int SSH_FXF_BLOCK_ADVISORY = 0x00000200;
    public static final int SSH_FXF_NOFOLLOW = 0x00000400;
    public static final int SSH_FXF_DELETE_ON_CLOSE = 0x00000800;
    public static final int SSH_FXF_ACCESS_AUDIT_ALARM_INFO = 0x00001000;
    public static final int SSH_FXF_ACCESS_BACKUP = 0x00002000;
    public static final int SSH_FXF_BACKUP_STREAM = 0x00004000;
    public static final int SSH_FXF_OVERRIDE_OWNER = 0x00008000;

    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 SSH_FXF_TEXT = 0x00000040;

    public static final int ACE4_READ_DATA = 0x00000001;
    public static final int ACE4_LIST_DIRECTORY = 0x00000001;
    public static final int ACE4_WRITE_DATA = 0x00000002;
    public static final int ACE4_ADD_FILE = 0x00000002;
    public static final int ACE4_APPEND_DATA = 0x00000004;
    public static final int ACE4_ADD_SUBDIRECTORY = 0x00000004;
    public static final int ACE4_READ_NAMED_ATTRS = 0x00000008;
    public static final int ACE4_WRITE_NAMED_ATTRS = 0x00000010;
    public static final int ACE4_EXECUTE = 0x00000020;
    public static final int ACE4_DELETE_CHILD = 0x00000040;
    public static final int ACE4_READ_ATTRIBUTES = 0x00000080;
    public static final int ACE4_WRITE_ATTRIBUTES = 0x00000100;
    public static final int ACE4_DELETE = 0x00010000;
    public static final int ACE4_READ_ACL = 0x00020000;
    public static final int ACE4_WRITE_ACL = 0x00040000;
    public static final int ACE4_WRITE_OWNER = 0x00080000;

    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;
    public static final int S_ISUID = 0004000;
    public static final int S_ISGID = 0002000;
    public static final int S_ISVTX = 0001000;

    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 {
        boolean done;

        public DirectoryHandle(SshFile file) {
            super(file);
        }

        public boolean isDone() {
            return done;
        }

        public void setDone(boolean done) {
            this.done = done;
        }
    }

    protected static class FileHandle extends Handle {
        int flags;

        public FileHandle(SshFile sshFile, int flags) {
            super(sshFile);
            this.flags = flags;
        }

        public int getFlags() {
            return flags;
        }
    }

    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: {
            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;
                    }
                }
            }

            if (version <= 4) {
                String path = buffer.getString();
                int pflags = buffer.getInt();
                // attrs
                try {
                    SshFile file = resolveFile(path);
                    if (file.doesExist()) {
                        if (((pflags & SSH_FXF_CREAT) != 0) && ((pflags & SSH_FXF_EXCL) != 0)) {
                            sendStatus(id, SSH_FX_FILE_ALREADY_EXISTS, path);
                            return;
                        }
                    } else {
                        if (((pflags & SSH_FXF_CREAT) != 0)) {
                            if (!file.isWritable()) {
                                sendStatus(id, SSH_FX_FAILURE, "Can not create " + path);
                            }
                        }
                    }
                    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();
                    }
                    String handle = UUID.randomUUID().toString();
                    handles.put(handle, new FileHandle(file, pflags)); // handle flags conversion
                    sendHandle(id, handle);
                } catch (IOException e) {
                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
                }
            } else {
                String path = buffer.getString();
                int acc = buffer.getInt();
                int flags = buffer.getInt();
                // attrs
                try {
                    SshFile file = resolveFile(path);
                    switch (flags & SSH_FXF_ACCESS_DISPOSITION) {
                    case SSH_FXF_CREATE_NEW: {
                        if (file.doesExist()) {
                            sendStatus(id, SSH_FX_FILE_ALREADY_EXISTS, path);
                            return;
                        } else if (!file.isWritable()) {
                            sendStatus(id, SSH_FX_FAILURE, "Can not create " + path);
                        }
                        break;
                    }
                    case SSH_FXF_CREATE_TRUNCATE: {
                        if (file.doesExist()) {
                            sendStatus(id, SSH_FX_FILE_ALREADY_EXISTS, path);
                            return;
                        } else if (!file.isWritable()) {
                            sendStatus(id, SSH_FX_FAILURE, "Can not create " + path);
                        }
                        file.truncate();
                        break;
                    }
                    case SSH_FXF_OPEN_EXISTING: {
                        if (!file.doesExist()) {
                            if (!file.getParentFile().doesExist()) {
                                sendStatus(id, SSH_FX_NO_SUCH_PATH, path);
                            } else {
                                sendStatus(id, SSH_FX_NO_SUCH_FILE, path);
                            }
                            return;
                        }
                        break;
                    }
                    case SSH_FXF_OPEN_OR_CREATE: {
                        break;
                    }
                    case SSH_FXF_TRUNCATE_EXISTING: {
                        if (!file.doesExist()) {
                            if (!file.getParentFile().doesExist()) {
                                sendStatus(id, SSH_FX_NO_SUCH_PATH, path);
                            } else {
                                sendStatus(id, SSH_FX_NO_SUCH_FILE, path);
                            }
                            return;
                        }
                        file.truncate();
                        break;
                    }
                    default:
                        throw new IllegalArgumentException("Unsupported open mode: " + flags);
                    }
                    String handle = UUID.randomUUID().toString();
                    handles.put(handle, new FileHandle(file, flags));
                    sendHandle(id, handle);
                } catch (IOException e) {
                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
                }
            }
            break;
        }
        case SSH_FXP_CLOSE: {
            String handle = buffer.getString();
            try {
                Handle h = handles.get(handle);
                if (h == null) {
                    sendStatus(id, SSH_FX_INVALID_HANDLE, 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();
            try {
                Handle p = handles.get(handle);
                if (!(p instanceof FileHandle)) {
                    sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
                } else {
                    SshFile ssh = ((FileHandle) p).getFile();
                    InputStream is = ssh.createInputStream(offset);
                    try {
                        byte[] b = new byte[Math.max(len, 1024 * 32)];
                        len = is.read(b);
                        if (len >= 0) {
                            Buffer buf = new Buffer(len + 5);
                            buf.putByte((byte) SSH_FXP_DATA);
                            buf.putInt(id);
                            buf.putBytes(b, 0, len);
                            buf.putBoolean(len == 0);
                            send(buf);
                        } else {
                            sendStatus(id, SSH_FX_EOF, "");
                        }
                    } finally {
                        if (is != null) {
                            is.close();
                        }
                    }
                }
            } 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();
            try {
                Handle p = handles.get(handle);
                if (!(p instanceof FileHandle)) {
                    sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
                } else {
                    SshFile sshFile = ((FileHandle) p).getFile();
                    OutputStream os = sshFile.createOutputStream(offset);

                    try {
                        os.write(data); // TODO: handle append flags
                    } finally {
                        if (os != null) {
                            os.close();
                        }
                    }

                    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:
        case SSH_FXP_STAT: {
            String path = buffer.getString();
            try {
                SshFile p = resolveFile(path);
                sendAttrs(id, p);
            } 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();
            try {
                Handle p = handles.get(handle);
                if (p == null) {
                    sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
                } else {
                    sendAttrs(id, p.getFile());
                }
            } 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_OPENDIR: {
            String path = buffer.getString();
            try {
                SshFile p = resolveFile(path);
                if (!p.doesExist()) {
                    sendStatus(id, SSH_FX_NO_SUCH_FILE, path);
                } else if (!p.isDirectory()) {
                    sendStatus(id, SSH_FX_NOT_A_DIRECTORY, 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();
            try {
                Handle p = handles.get(handle);
                if (!(p instanceof DirectoryHandle)) {
                    sendStatus(id, SSH_FX_INVALID_HANDLE, 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_NOT_A_DIRECTORY, p.getFile().getAbsolutePath());
                } else if (!p.getFile().isReadable()) {
                    sendStatus(id, SSH_FX_PERMISSION_DENIED, p.getFile().getAbsolutePath());
                } else {
                    sendName(id, p.getFile().listSshFiles());
                    ((DirectoryHandle) p).setDone(true);
                }
            } catch (IOException e) {
                sendStatus(id, SSH_FX_FAILURE, e.getMessage());
            }
            break;
        }
        case SSH_FXP_REMOVE: {
            String path = buffer.getString();
            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_FILE_IS_A_DIRECTORY, 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();
            // attrs
            try {
                SshFile p = resolveFile(path);
                if (p.doesExist()) {
                    if (p.isDirectory()) {
                        sendStatus(id, SSH_FX_FILE_ALREADY_EXISTS, p.getAbsolutePath());
                    } else {
                        sendStatus(id, SSH_FX_NOT_A_DIRECTORY, 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 {
                    sendStatus(id, SSH_FX_OK, "");
                }
            } catch (IOException e) {
                sendStatus(id, SSH_FX_FAILURE, e.getMessage());
            }
            break;
        }
        case SSH_FXP_RMDIR: {
            String path = buffer.getString();
            // 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_DIR_NOT_EMPTY, path);
                        }
                    } else {
                        sendStatus(id, SSH_FX_NO_SUCH_PATH, path);
                    }
                } else {
                    sendStatus(id, SSH_FX_NOT_A_DIRECTORY, p.getAbsolutePath());
                }
            } catch (IOException e) {
                sendStatus(id, SSH_FX_FAILURE, e.getMessage());
            }
            break;
        }
        case SSH_FXP_REALPATH: {
            String path = buffer.getString();
            if (path.trim().length() == 0) {
                path = ".";
            }
            // TODO: handle optional args
            try {
                SshFile p = resolveFile(path);
                sendPath(id, p);
            } 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_RENAME: {
            String oldPath = buffer.getString();
            String newPath = buffer.getString();
            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_FILE_ALREADY_EXISTS, 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_SETSTAT:
        case SSH_FXP_FSETSTAT: {
            // This is required for WinSCP / Cyberduck to upload properly
            // Blindly reply "OK"
            // TODO implement it
            sendStatus(id, SSH_FX_OK, "");
            break;
        }

        default:
            LogUtils.error(log, "Received: {0}", type);
            sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command " + type + " is unsupported or not implemented");
            throw new IllegalStateException();
        }
    }

    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) throws IOException {
        Buffer buffer = new Buffer();
        buffer.putByte((byte) SSH_FXP_ATTRS);
        buffer.putInt(id);
        writeAttrs(buffer, file);
        send(buffer);
    }

    protected void sendAttrs(int id, SshFile file, int flags) throws IOException {
        Buffer buffer = new Buffer();
        buffer.putByte((byte) SSH_FXP_ATTRS);
        buffer.putInt(id);
        writeAttrs(buffer, file, flags);
        send(buffer);
    }

    protected void sendPath(int id, SshFile f) 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(".");
        }
        if (version <= 3) {
            buffer.putString(getLongName(f)); // Format specified in the specs
            buffer.putInt(0);
        } else {
            buffer.putString(f.getName()); // Supposed to be UTF-8
            writeAttrs(buffer, f);
        }
        send(buffer);
    }

    protected void sendName(int id, Collection<SshFile> files) throws IOException {
        Buffer buffer = new Buffer();
        buffer.putByte((byte) SSH_FXP_NAME);
        buffer.putInt(id);
        buffer.putInt(files.size());
        for (SshFile f : files) {
            buffer.putString(f.getName());
            if (version <= 3) {
                buffer.putString(getLongName(f)); // Format specified in the specs
            } else {
                buffer.putString(f.getName()); // Supposed to be UTF-8
            }
            writeAttrs(buffer, f);
        }
        send(buffer);
    }

    private String getLongName(SshFile f) {
        String username = session.getUsername();
        if (username.length() > 8) {
            username = username.substring(0, 8);
        } else {
            for (int i = username.length(); i < 8; i++) {
                username = username + " ";
            }
        }

        long length = f.getSize();
        String lengthString = String.format("%1$#8s", length);

        StringBuilder sb = new StringBuilder();
        sb.append((f.isDirectory() ? "d" : "-"));
        sb.append((f.isReadable() ? "r" : "-"));
        sb.append((f.isWritable() ? "w" : "-"));
        sb.append((/*f.canExecute() ? "x" :*/ "-"));
        sb.append((f.isReadable() ? "r" : "-"));
        sb.append((f.isWritable() ? "w" : "-"));
        sb.append((/*f.canExecute() ? "x" :*/ "-"));
        sb.append((f.isReadable() ? "r" : "-"));
        sb.append((f.isWritable() ? "w" : "-"));
        sb.append((/*f.canExecute() ? "x" :*/ "-"));
        sb.append(" ");
        sb.append("  1");
        sb.append(" ");
        sb.append(username);
        sb.append(" ");
        sb.append(username);
        sb.append(" ");
        sb.append(lengthString);
        sb.append(" ");
        sb.append(getUnixDate(f.getLastModified()));
        sb.append(" ");
        sb.append(f.getName());

        return sb.toString();
    }

    protected void writeAttrs(Buffer buffer, SshFile file) throws IOException {
        writeAttrs(buffer, file, 0);
    }

    protected void writeAttrs(Buffer buffer, SshFile file, int flags) throws IOException {
        if (!file.doesExist()) {
            throw new FileNotFoundException(file.getAbsolutePath());
        }
        if (version >= 4) {
            long size = file.getSize();
            String username = session.getUsername();
            long lastModif = file.getLastModified();
            int p = 0;
            if (file.isReadable()) {
                p |= S_IRUSR;
            }
            if (file.isWritable()) {
                p |= S_IWUSR;
            }
            /*
            if (file.canExecute()) {
            p |= S_IXUSR;
            }
            */
            if (file.isFile()) {
                buffer.putInt(SSH_FILEXFER_ATTR_PERMISSIONS);
                buffer.putByte((byte) SSH_FILEXFER_TYPE_REGULAR);
                buffer.putInt(p);
            } else if (file.isDirectory()) {
                buffer.putInt(SSH_FILEXFER_ATTR_PERMISSIONS);
                buffer.putByte((byte) SSH_FILEXFER_TYPE_DIRECTORY);
                buffer.putInt(p);
            } else {
                buffer.putInt(0);
                buffer.putByte((byte) SSH_FILEXFER_TYPE_UNKNOWN);
            }
        } else {
            int p = 0;
            if (file.isFile()) {
                p |= 0100000;
            }
            if (file.isDirectory()) {
                p |= 0040000;
            }
            if (file.isReadable()) {
                p |= 0000400;
            }
            if (file.isWritable()) {
                p |= 0000200;
            }
            /*
            if (file.canExecute()) {
            p |= 0000100;
            }
            */
            if (file.isFile()) {
                buffer.putInt(SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_ACMODTIME);
                buffer.putLong(file.getSize());
                buffer.putInt(p);
                buffer.putInt(file.getLastModified() / 1000);
                buffer.putInt(file.getLastModified() / 1000);
            } else if (file.isDirectory()) {
                buffer.putInt(SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_ACMODTIME);
                buffer.putInt(p);
                buffer.putInt(file.getLastModified() / 1000);
                buffer.putInt(file.getLastModified() / 1000);
            } else {
                buffer.putInt(0);
            }
        }
    }

    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 {
        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.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();
    }

}