http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2529a4c3/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java new file mode 100644 index 0000000..a54a7d5 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java @@ -0,0 +1,2582 @@ +/* + * 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.subsystem.sftp; + +import java.io.EOFException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.StreamCorruptedException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.AccessDeniedException; +import java.nio.file.CopyOption; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.AclEntry; +import java.nio.file.attribute.AclFileAttributeView; +import java.nio.file.attribute.FileOwnerAttributeView; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.UserPrincipal; +import java.nio.file.attribute.UserPrincipalLookupService; +import java.nio.file.attribute.UserPrincipalNotFoundException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.IntUnaryOperator; + +import org.apache.sshd.common.FactoryManager; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.NamedResource; +import org.apache.sshd.common.OptionalFeature; +import org.apache.sshd.common.PropertyResolver; +import org.apache.sshd.common.PropertyResolverUtils; +import org.apache.sshd.common.config.VersionProperties; +import org.apache.sshd.common.digest.BuiltinDigests; +import org.apache.sshd.common.digest.Digest; +import org.apache.sshd.common.subsystem.sftp.SftpConstants; +import org.apache.sshd.common.subsystem.sftp.SftpException; +import org.apache.sshd.common.subsystem.sftp.SftpHelper; +import org.apache.sshd.common.subsystem.sftp.extensions.AclSupportedParser; +import org.apache.sshd.common.subsystem.sftp.extensions.SpaceAvailableExtensionInfo; +import org.apache.sshd.common.subsystem.sftp.extensions.openssh.AbstractOpenSSHExtensionParser.OpenSSHExtension; +import org.apache.sshd.common.subsystem.sftp.extensions.openssh.FsyncExtensionParser; +import org.apache.sshd.common.subsystem.sftp.extensions.openssh.HardLinkExtensionParser; +import org.apache.sshd.common.util.EventListenerUtils; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.NumberUtils; +import org.apache.sshd.common.util.OsUtils; +import org.apache.sshd.common.util.Pair; +import org.apache.sshd.common.util.SelectorUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.apache.sshd.common.util.io.FileInfoExtractor; +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.common.util.logging.AbstractLoggingBean; +import org.apache.sshd.server.session.ServerSession; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public abstract class AbstractSftpSubsystemHelper + extends AbstractLoggingBean + implements SftpEventListenerManager, SftpSubsystemEnvironment { + /** + * Whether to automatically follow symbolic links when resolving paths + * @see #DEFAULT_AUTO_FOLLOW_LINKS + */ + public static final String AUTO_FOLLOW_LINKS = "sftp-auto-follow-links"; + + /** + * Default value of {@value #AUTO_FOLLOW_LINKS} + */ + public static final boolean DEFAULT_AUTO_FOLLOW_LINKS = true; + + /** + * Allows controlling reports of which client extensions are supported + * (and reported via "support" and "support2" server + * extensions) as a comma-separate list of names. <B>Note:</B> requires + * overriding the {@link #executeExtendedCommand(Buffer, int, String)} + * command accordingly. If empty string is set then no server extensions + * are reported + * + * @see #DEFAULT_SUPPORTED_CLIENT_EXTENSIONS + */ + public static final String CLIENT_EXTENSIONS_PROP = "sftp-client-extensions"; + + /** + * The default reported supported client extensions + */ + public static final Map<String, OptionalFeature> DEFAULT_SUPPORTED_CLIENT_EXTENSIONS = + // TODO text-seek - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-13.txt + // TODO home-directory - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt + GenericUtils.<String, OptionalFeature>mapBuilder() + .put(SftpConstants.EXT_VERSION_SELECT, OptionalFeature.TRUE) + .put(SftpConstants.EXT_COPY_FILE, OptionalFeature.TRUE) + .put(SftpConstants.EXT_MD5_HASH, BuiltinDigests.md5) + .put(SftpConstants.EXT_MD5_HASH_HANDLE, BuiltinDigests.md5) + .put(SftpConstants.EXT_CHECK_FILE_HANDLE, OptionalFeature.any(BuiltinDigests.VALUES)) + .put(SftpConstants.EXT_CHECK_FILE_NAME, OptionalFeature.any(BuiltinDigests.VALUES)) + .put(SftpConstants.EXT_COPY_DATA, OptionalFeature.TRUE) + .put(SftpConstants.EXT_SPACE_AVAILABLE, OptionalFeature.TRUE) + .immutable(); + + /** + * Comma-separated list of which {@code OpenSSH} extensions are reported and + * what version is reported for each - format: {@code name=version}. If empty + * value set, then no such extensions are reported. Otherwise, the + * {@link #DEFAULT_OPEN_SSH_EXTENSIONS} are used + */ + public static final String OPENSSH_EXTENSIONS_PROP = "sftp-openssh-extensions"; + public static final List<OpenSSHExtension> DEFAULT_OPEN_SSH_EXTENSIONS = + Collections.unmodifiableList( + Arrays.asList( + new OpenSSHExtension(FsyncExtensionParser.NAME, "1"), + new OpenSSHExtension(HardLinkExtensionParser.NAME, "1") + )); + + public static final List<String> DEFAULT_OPEN_SSH_EXTENSIONS_NAMES = + Collections.unmodifiableList(NamedResource.getNameList(DEFAULT_OPEN_SSH_EXTENSIONS)); + + /** + * Comma separate list of {@code SSH_ACL_CAP_xxx} names - where name can be without + * the prefix. If not defined then {@link #DEFAULT_ACL_SUPPORTED_MASK} is used + */ + public static final String ACL_SUPPORTED_MASK_PROP = "sftp-acl-supported-mask"; + public static final Set<Integer> DEFAULT_ACL_SUPPORTED_MASK = + Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + SftpConstants.SSH_ACL_CAP_ALLOW, + SftpConstants.SSH_ACL_CAP_DENY, + SftpConstants.SSH_ACL_CAP_AUDIT, + SftpConstants.SSH_ACL_CAP_ALARM))); + + /** + * Property that can be used to set the reported NL value. + * If not set, then {@link IoUtils#EOL} is used + */ + public static final String NEWLINE_VALUE = "sftp-newline"; + + /** + * Force the use of a max. packet length for {@link #doRead(Buffer, int)} protection + * against malicious packets + * + * @see #DEFAULT_MAX_READDATA_PACKET_LENGTH + */ + public static final String MAX_READDATA_PACKET_LENGTH_PROP = "sftp-max-readdata-packet-length"; + public static final int DEFAULT_MAX_READDATA_PACKET_LENGTH = 63 * 1024; + + private final UnsupportedAttributePolicy unsupportedAttributePolicy; + private final Collection<SftpEventListener> sftpEventListeners = new CopyOnWriteArraySet<>(); + private final SftpEventListener sftpEventListenerProxy; + private final SftpFileSystemAccessor fileSystemAccessor; + private final SftpErrorStatusDataHandler errorStatusDataHandler; + + protected AbstractSftpSubsystemHelper( + UnsupportedAttributePolicy policy, SftpFileSystemAccessor accessor, SftpErrorStatusDataHandler handler) { + unsupportedAttributePolicy = Objects.requireNonNull(policy, "No unsupported attribute policy provided"); + fileSystemAccessor = Objects.requireNonNull(accessor, "No file system accessor"); + sftpEventListenerProxy = EventListenerUtils.proxyWrapper(SftpEventListener.class, getClass().getClassLoader(), sftpEventListeners); + errorStatusDataHandler = Objects.requireNonNull(handler, "No error status data handler"); + } + + @Override + public UnsupportedAttributePolicy getUnsupportedAttributePolicy() { + return unsupportedAttributePolicy; + } + + @Override + public SftpFileSystemAccessor getFileSystemAccessor() { + return fileSystemAccessor; + } + + @Override + public SftpEventListener getSftpEventListenerProxy() { + return sftpEventListenerProxy; + } + + @Override + public boolean addSftpEventListener(SftpEventListener listener) { + return sftpEventListeners.add(SftpEventListener.validateListener(listener)); + } + + @Override + public boolean removeSftpEventListener(SftpEventListener listener) { + if (listener == null) { + return false; + } + + return sftpEventListeners.remove(SftpEventListener.validateListener(listener)); + } + + public SftpErrorStatusDataHandler getErrorStatusDataHandler() { + return errorStatusDataHandler; + } + + protected abstract void process(Buffer buffer) throws IOException; + + /** + * @param buffer The {@link Buffer} holding the request + * @param id The request id + * @param proposed The proposed value + * @return A {@link Boolean} indicating whether to accept/reject the proposal. + * If {@code null} then rejection response has been sent, otherwise and + * appropriate response is generated + * @throws IOException If failed send an independent rejection response + */ + protected Boolean validateProposedVersion(Buffer buffer, int id, String proposed) throws IOException { + if (log.isDebugEnabled()) { + log.debug("validateProposedVersion({})[id={}] SSH_FXP_EXTENDED(version-select) (version={})", + getServerSession(), id, proposed); + } + + if (GenericUtils.length(proposed) != 1) { + return Boolean.FALSE; + } + + char digit = proposed.charAt(0); + if ((digit < '0') || (digit > '9')) { + return Boolean.FALSE; + } + + int value = digit - '0'; + String all = checkVersionCompatibility(buffer, id, value, SftpConstants.SSH_FX_FAILURE); + if (GenericUtils.isEmpty(all)) { // validation failed + return null; + } else { + return Boolean.TRUE; + } + } + + /** + * Checks if a proposed version is within supported range. <B>Note:</B> + * if the user forced a specific value via the {@link SftpSubsystemEnvironment#SFTP_VERSION} + * property, then it is used to validate the proposed value + * + * @param buffer The {@link Buffer} containing the request + * @param id The SSH message ID to be used to send the failure message + * if required + * @param proposed The proposed version value + * @param failureOpcode The failure opcode to send if validation fails + * @return A {@link String} of comma separated values representing all + * the supported version - {@code null} if validation failed and an + * appropriate status message was sent + * @throws IOException If failed to send the failure status message + */ + protected String checkVersionCompatibility(Buffer buffer, int id, int proposed, int failureOpcode) throws IOException { + int low = SftpSubsystemEnvironment.LOWER_SFTP_IMPL; + int hig = SftpSubsystemEnvironment.HIGHER_SFTP_IMPL; + String available = SftpSubsystemEnvironment.ALL_SFTP_IMPL; + // check if user wants to use a specific version + ServerSession session = getServerSession(); + Integer sftpVersion = session.getInteger(SftpSubsystemEnvironment.SFTP_VERSION); + if (sftpVersion != null) { + int forcedValue = sftpVersion; + if ((forcedValue < SftpSubsystemEnvironment.LOWER_SFTP_IMPL) || (forcedValue > SftpSubsystemEnvironment.HIGHER_SFTP_IMPL)) { + throw new IllegalStateException("Forced SFTP version (" + sftpVersion + ") not within supported values: " + available); + } + hig = sftpVersion; + low = hig; + available = sftpVersion.toString(); + } + + if (log.isTraceEnabled()) { + log.trace("checkVersionCompatibility({})[id={}] - proposed={}, available={}", + getServerSession(), id, proposed, available); + } + + if ((proposed < low) || (proposed > hig)) { + sendStatus(BufferUtils.clear(buffer), id, failureOpcode, "Proposed version (" + proposed + ") not in supported range: " + available); + return null; + } + + return available; + } + + protected void doOpen(Buffer buffer, int id) throws IOException { + String path = buffer.getString(); + /* + * Be consistent with FileChannel#open - if no mode specified then READ is assumed + */ + int access = 0; + int version = getVersion(); + if (version >= SftpConstants.SFTP_V5) { + access = buffer.getInt(); + if (access == 0) { + access = SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES; + } + } + + int pflags = buffer.getInt(); + if (pflags == 0) { + pflags = SftpConstants.SSH_FXF_READ; + } + + if (version < SftpConstants.SFTP_V5) { + int flags = pflags; + pflags = 0; + switch (flags & (SftpConstants.SSH_FXF_READ | SftpConstants.SSH_FXF_WRITE)) { + case SftpConstants.SSH_FXF_READ: + access |= SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES; + break; + case SftpConstants.SSH_FXF_WRITE: + access |= SftpConstants.ACE4_WRITE_DATA | SftpConstants.ACE4_WRITE_ATTRIBUTES; + break; + default: + access |= SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES; + access |= SftpConstants.ACE4_WRITE_DATA | SftpConstants.ACE4_WRITE_ATTRIBUTES; + break; + } + if ((flags & SftpConstants.SSH_FXF_APPEND) != 0) { + access |= SftpConstants.ACE4_APPEND_DATA; + pflags |= SftpConstants.SSH_FXF_APPEND_DATA | SftpConstants.SSH_FXF_APPEND_DATA_ATOMIC; + } + if ((flags & SftpConstants.SSH_FXF_CREAT) != 0) { + if ((flags & SftpConstants.SSH_FXF_EXCL) != 0) { + pflags |= SftpConstants.SSH_FXF_CREATE_NEW; + } else if ((flags & SftpConstants.SSH_FXF_TRUNC) != 0) { + pflags |= SftpConstants.SSH_FXF_CREATE_TRUNCATE; + } else { + pflags |= SftpConstants.SSH_FXF_OPEN_OR_CREATE; + } + } else { + if ((flags & SftpConstants.SSH_FXF_TRUNC) != 0) { + pflags |= SftpConstants.SSH_FXF_TRUNCATE_EXISTING; + } else { + pflags |= SftpConstants.SSH_FXF_OPEN_EXISTING; + } + } + } + + Map<String, Object> attrs = readAttrs(buffer); + String handle; + try { + handle = doOpen(id, path, pflags, access, attrs); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_OPEN, path); + return; + } + + sendHandle(BufferUtils.clear(buffer), id, handle); + } + + /** + * @param id Request id + * @param path Path + * @param pflags Open mode flags - see {@code SSH_FXF_XXX} flags + * @param access Access mode flags - see {@code ACE4_XXX} flags + * @param attrs Requested attributes + * @return The assigned (opaque) handle + * @throws IOException if failed to execute + */ + protected abstract String doOpen(int id, String path, int pflags, int access, Map<String, Object> attrs) throws IOException; + + protected void doClose(Buffer buffer, int id) throws IOException { + String handle = buffer.getString(); + try { + doClose(id, handle); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_CLOSE, handle); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "", ""); + } + + protected abstract void doClose(int id, String handle) throws IOException; + + protected void doRead(Buffer buffer, int id) throws IOException { + String handle = buffer.getString(); + long offset = buffer.getLong(); + int requestedLength = buffer.getInt(); + ServerSession serverSession = getServerSession(); + int maxAllowed = serverSession.getIntProperty(MAX_READDATA_PACKET_LENGTH_PROP, DEFAULT_MAX_READDATA_PACKET_LENGTH); + int readLen = Math.min(requestedLength, maxAllowed); + if (log.isTraceEnabled()) { + log.trace("doRead({})[id={}]({})[offset={}] - req={}, max={}, effective={}", + serverSession, id, handle, offset, requestedLength, maxAllowed, readLen); + } + + try { + ValidateUtils.checkTrue(readLen >= 0, "Illegal requested read length: %d", readLen); + + buffer.clear(); + buffer.ensureCapacity(readLen + Long.SIZE /* the header */, IntUnaryOperator.identity()); + + buffer.putByte((byte) SftpConstants.SSH_FXP_DATA); + buffer.putInt(id); + int lenPos = buffer.wpos(); + buffer.putInt(0); + + int startPos = buffer.wpos(); + int len = doRead(id, handle, offset, readLen, buffer.array(), startPos); + if (len < 0) { + throw new EOFException("Unable to read " + readLen + " bytes from offset=" + offset + " of " + handle); + } + buffer.wpos(startPos + len); + BufferUtils.updateLengthPlaceholder(buffer, lenPos, len); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_READ, handle, offset, requestedLength); + return; + } + + send(buffer); + } + + protected abstract int doRead(int id, String handle, long offset, int length, byte[] data, int doff) throws IOException; + + protected void doWrite(Buffer buffer, int id) throws IOException { + String handle = buffer.getString(); + long offset = buffer.getLong(); + int length = buffer.getInt(); + try { + doWrite(id, handle, offset, length, buffer.array(), buffer.rpos(), buffer.available()); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_WRITE, handle, offset, length); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected abstract void doWrite(int id, String handle, long offset, int length, byte[] data, int doff, int remaining) throws IOException; + + protected void doLStat(Buffer buffer, int id) throws IOException { + String path = buffer.getString(); + int flags = SftpConstants.SSH_FILEXFER_ATTR_ALL; + int version = getVersion(); + if (version >= SftpConstants.SFTP_V4) { + flags = buffer.getInt(); + } + + Map<String, ?> attrs; + try { + attrs = doLStat(id, path, flags); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_LSTAT, path, flags); + return; + } + + sendAttrs(BufferUtils.clear(buffer), id, attrs); + } + + protected Map<String, Object> doLStat(int id, String path, int flags) throws IOException { + Path p = resolveFile(path); + if (log.isDebugEnabled()) { + log.debug("doLStat({})[id={}] SSH_FXP_LSTAT (path={}[{}], flags=0x{})", + getServerSession(), id, path, p, Integer.toHexString(flags)); + } + + /* + * SSH_FXP_STAT and SSH_FXP_LSTAT only differ in that SSH_FXP_STAT + * follows symbolic links on the server, whereas SSH_FXP_LSTAT does not. + */ + return resolveFileAttributes(p, flags, IoUtils.getLinkOptions(false)); + } + + protected void doSetStat(Buffer buffer, int id) throws IOException { + String path = buffer.getString(); + Map<String, Object> attrs = readAttrs(buffer); + try { + doSetStat(id, path, attrs); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_SETSTAT, path); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected void doSetStat(int id, String path, Map<String, ?> attrs) throws IOException { + if (log.isDebugEnabled()) { + log.debug("doSetStat({})[id={}] SSH_FXP_SETSTAT (path={}, attrs={})", + getServerSession(), id, path, attrs); + } + Path p = resolveFile(path); + doSetAttributes(p, attrs); + } + + protected void doFStat(Buffer buffer, int id) throws IOException { + String handle = buffer.getString(); + int flags = SftpConstants.SSH_FILEXFER_ATTR_ALL; + int version = getVersion(); + if (version >= SftpConstants.SFTP_V4) { + flags = buffer.getInt(); + } + + Map<String, ?> attrs; + try { + attrs = doFStat(id, handle, flags); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_FSTAT, handle, flags); + return; + } + + sendAttrs(BufferUtils.clear(buffer), id, attrs); + } + + protected abstract Map<String, Object> doFStat(int id, String handle, int flags) throws IOException; + + protected void doFSetStat(Buffer buffer, int id) throws IOException { + String handle = buffer.getString(); + Map<String, Object> attrs = readAttrs(buffer); + try { + doFSetStat(id, handle, attrs); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_FSETSTAT, handle, attrs); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected abstract void doFSetStat(int id, String handle, Map<String, ?> attrs) throws IOException; + + protected void doOpenDir(Buffer buffer, int id) throws IOException { + String path = buffer.getString(); + String handle; + + try { + Path p = resolveNormalizedLocation(path); + if (log.isDebugEnabled()) { + log.debug("doOpenDir({})[id={}] SSH_FXP_OPENDIR (path={})[{}]", + getServerSession(), id, path, p); + } + + LinkOption[] options = + getPathResolutionLinkOption(SftpConstants.SSH_FXP_OPENDIR, "", p); + handle = doOpenDir(id, path, p, options); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_OPENDIR, path); + return; + } + + sendHandle(BufferUtils.clear(buffer), id, handle); + } + + protected abstract String doOpenDir(int id, String path, Path p, LinkOption... options) throws IOException; + + protected abstract void doReadDir(Buffer buffer, int id) throws IOException; + + protected void doLink(Buffer buffer, int id) throws IOException { + String targetPath = buffer.getString(); + String linkPath = buffer.getString(); + boolean symLink = buffer.getBoolean(); + + try { + if (log.isDebugEnabled()) { + log.debug("doLink({})[id={}] SSH_FXP_LINK linkpath={}, targetpath={}, symlink={}", + getServerSession(), id, linkPath, targetPath, symLink); + } + + doLink(id, targetPath, linkPath, symLink); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_LINK, targetPath, linkPath, symLink); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected void doLink(int id, String targetPath, String linkPath, boolean symLink) throws IOException { + createLink(id, targetPath, linkPath, symLink); + } + + protected void doSymLink(Buffer buffer, int id) throws IOException { + String targetPath = buffer.getString(); + String linkPath = buffer.getString(); + try { + if (log.isDebugEnabled()) { + log.debug("doSymLink({})[id={}] SSH_FXP_SYMLINK linkpath={}, targetpath={}", + getServerSession(), id, targetPath, linkPath); + } + doSymLink(id, targetPath, linkPath); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_SYMLINK, targetPath, linkPath); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected void doSymLink(int id, String targetPath, String linkPath) throws IOException { + createLink(id, targetPath, linkPath, true); + } + + protected abstract void createLink(int id, String existingPath, String linkPath, boolean symLink) throws IOException; + + // see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL section 10 + protected void doOpenSSHHardLink(Buffer buffer, int id) throws IOException { + String srcFile = buffer.getString(); + String dstFile = buffer.getString(); + + try { + doOpenSSHHardLink(id, srcFile, dstFile); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_EXTENDED, HardLinkExtensionParser.NAME, srcFile, dstFile); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected void doOpenSSHHardLink(int id, String srcFile, String dstFile) throws IOException { + if (log.isDebugEnabled()) { + log.debug("doOpenSSHHardLink({})[id={}] SSH_FXP_EXTENDED[{}] (src={}, dst={})", + getServerSession(), id, HardLinkExtensionParser.NAME, srcFile, dstFile); + } + + createLink(id, srcFile, dstFile, false); + } + + protected void doSpaceAvailable(Buffer buffer, int id) throws IOException { + String path = buffer.getString(); + SpaceAvailableExtensionInfo info; + try { + info = doSpaceAvailable(id, path); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_EXTENDED, SftpConstants.EXT_SPACE_AVAILABLE, path); + return; + } + + buffer.clear(); + buffer.putByte((byte) SftpConstants.SSH_FXP_EXTENDED_REPLY); + buffer.putInt(id); + SpaceAvailableExtensionInfo.encode(buffer, info); + send(buffer); + } + + protected SpaceAvailableExtensionInfo doSpaceAvailable(int id, String path) throws IOException { + Path nrm = resolveNormalizedLocation(path); + if (log.isDebugEnabled()) { + log.debug("doSpaceAvailable({})[id={}] path={}[{}]", getServerSession(), id, path, nrm); + } + + FileStore store = Files.getFileStore(nrm); + if (log.isTraceEnabled()) { + log.trace("doSpaceAvailable({})[id={}] path={}[{}] - {}[{}]", + getServerSession(), id, path, nrm, store.name(), store.type()); + } + + return new SpaceAvailableExtensionInfo(store); + } + + protected void doTextSeek(Buffer buffer, int id) throws IOException { + String handle = buffer.getString(); + long line = buffer.getLong(); + try { + // TODO : implement text-seek - see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-03#section-6.3 + doTextSeek(id, handle, line); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_EXTENDED, SftpConstants.EXT_TEXT_SEEK, handle, line); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected abstract void doTextSeek(int id, String handle, long line) throws IOException; + + // see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL section 10 + protected void doOpenSSHFsync(Buffer buffer, int id) throws IOException { + String handle = buffer.getString(); + try { + doOpenSSHFsync(id, handle); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_EXTENDED, FsyncExtensionParser.NAME, handle); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected abstract void doOpenSSHFsync(int id, String handle) throws IOException; + + protected void doCheckFileHash(Buffer buffer, int id, String targetType) throws IOException { + String target = buffer.getString(); + String algList = buffer.getString(); + String[] algos = GenericUtils.split(algList, ','); + long startOffset = buffer.getLong(); + long length = buffer.getLong(); + int blockSize = buffer.getInt(); + try { + buffer.clear(); + buffer.putByte((byte) SftpConstants.SSH_FXP_EXTENDED_REPLY); + buffer.putInt(id); + buffer.putString(SftpConstants.EXT_CHECK_FILE); + doCheckFileHash(id, targetType, target, Arrays.asList(algos), startOffset, length, blockSize, buffer); + } catch (Exception e) { + sendStatus(BufferUtils.clear(buffer), id, e, + SftpConstants.SSH_FXP_EXTENDED, targetType, target, algList, startOffset, length, blockSize); + return; + } + + send(buffer); + } + + protected void doCheckFileHash(int id, Path file, NamedFactory<? extends Digest> factory, + long startOffset, long length, int blockSize, Buffer buffer) + throws Exception { + ValidateUtils.checkTrue(startOffset >= 0L, "Invalid start offset: %d", startOffset); + ValidateUtils.checkTrue(length >= 0L, "Invalid length: %d", length); + ValidateUtils.checkTrue((blockSize == 0) || (blockSize >= SftpConstants.MIN_CHKFILE_BLOCKSIZE), "Invalid block size: %d", blockSize); + Objects.requireNonNull(factory, "No digest factory provided"); + buffer.putString(factory.getName()); + + long effectiveLength = length; + long totalLength = Files.size(file); + if (effectiveLength == 0L) { + effectiveLength = totalLength - startOffset; + } else { + long maxRead = startOffset + length; + if (maxRead > totalLength) { + effectiveLength = totalLength - startOffset; + } + } + ValidateUtils.checkTrue(effectiveLength > 0L, "Non-positive effective hash data length: %d", effectiveLength); + + byte[] digestBuf = (blockSize == 0) + ? new byte[Math.min((int) effectiveLength, IoUtils.DEFAULT_COPY_SIZE)] + : new byte[Math.min((int) effectiveLength, blockSize)]; + ByteBuffer wb = ByteBuffer.wrap(digestBuf); + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + try (SeekableByteChannel channel = accessor.openFile(getServerSession(), this, file, "", Collections.emptySet())) { + channel.position(startOffset); + + Digest digest = factory.create(); + digest.init(); + + if (blockSize == 0) { + while (effectiveLength > 0L) { + int remainLen = Math.min(digestBuf.length, (int) effectiveLength); + ByteBuffer bb = wb; + if (remainLen < digestBuf.length) { + bb = ByteBuffer.wrap(digestBuf, 0, remainLen); + } + bb.clear(); // prepare for next read + + int readLen = channel.read(bb); + if (readLen < 0) { + break; + } + + effectiveLength -= readLen; + digest.update(digestBuf, 0, readLen); + } + + byte[] hashValue = digest.digest(); + if (log.isTraceEnabled()) { + log.trace("doCheckFileHash({})[{}] offset={}, length={} - algo={}, hash={}", + getServerSession(), file, startOffset, length, + digest.getAlgorithm(), BufferUtils.toHex(':', hashValue)); + } + buffer.putBytes(hashValue); + } else { + for (int count = 0; effectiveLength > 0L; count++) { + int remainLen = Math.min(digestBuf.length, (int) effectiveLength); + ByteBuffer bb = wb; + if (remainLen < digestBuf.length) { + bb = ByteBuffer.wrap(digestBuf, 0, remainLen); + } + bb.clear(); // prepare for next read + + int readLen = channel.read(bb); + if (readLen < 0) { + break; + } + + effectiveLength -= readLen; + digest.update(digestBuf, 0, readLen); + + byte[] hashValue = digest.digest(); // NOTE: this also resets the hash for the next read + if (log.isTraceEnabled()) { + log.trace("doCheckFileHash({})({})[{}] offset={}, length={} - algo={}, hash={}", + getServerSession(), file, count, startOffset, length, + digest.getAlgorithm(), BufferUtils.toHex(':', hashValue)); + } + buffer.putBytes(hashValue); + } + } + } + } + + protected void doMD5Hash(Buffer buffer, int id, String targetType) throws IOException { + String target = buffer.getString(); + long startOffset = buffer.getLong(); + long length = buffer.getLong(); + byte[] quickCheckHash = buffer.getBytes(); + byte[] hashValue; + + try { + hashValue = doMD5Hash(id, targetType, target, startOffset, length, quickCheckHash); + if (log.isTraceEnabled()) { + log.trace("doMD5Hash({})({})[{}] offset={}, length={}, quick-hash={} - hash={}", + getServerSession(), targetType, target, startOffset, length, + BufferUtils.toHex(':', quickCheckHash), + BufferUtils.toHex(':', hashValue)); + } + + } catch (Exception e) { + sendStatus(BufferUtils.clear(buffer), id, e, + SftpConstants.SSH_FXP_EXTENDED, targetType, target, startOffset, length, quickCheckHash); + return; + } + + buffer.clear(); + buffer.putByte((byte) SftpConstants.SSH_FXP_EXTENDED_REPLY); + buffer.putInt(id); + buffer.putString(targetType); + buffer.putBytes(hashValue); + send(buffer); + } + + protected abstract byte[] doMD5Hash( + int id, String targetType, String target, long startOffset, long length, byte[] quickCheckHash) + throws Exception; + + protected byte[] doMD5Hash(int id, Path path, long startOffset, long length, byte[] quickCheckHash) throws Exception { + ValidateUtils.checkTrue(startOffset >= 0L, "Invalid start offset: %d", startOffset); + ValidateUtils.checkTrue(length > 0L, "Invalid length: %d", length); + if (!BuiltinDigests.md5.isSupported()) { + throw new UnsupportedOperationException(BuiltinDigests.md5.getAlgorithm() + " hash not supported"); + } + + Digest digest = BuiltinDigests.md5.create(); + digest.init(); + + long effectiveLength = length; + byte[] digestBuf = new byte[(int) Math.min(effectiveLength, SftpConstants.MD5_QUICK_HASH_SIZE)]; + ByteBuffer wb = ByteBuffer.wrap(digestBuf); + boolean hashMatches = false; + byte[] hashValue = null; + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + try (SeekableByteChannel channel = accessor.openFile(getServerSession(), this, path, null, EnumSet.of(StandardOpenOption.READ))) { + channel.position(startOffset); + + /* + * To quote http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.1: + * + * If this is a zero length string, the client does not have the + * data, and is requesting the hash for reasons other than comparing + * with a local file. The server MAY return SSH_FX_OP_UNSUPPORTED in + * this case. + */ + if (NumberUtils.length(quickCheckHash) <= 0) { + // TODO consider limiting it - e.g., if the requested effective length is <= than some (configurable) threshold + hashMatches = true; + } else { + int readLen = channel.read(wb); + if (readLen < 0) { + throw new EOFException("EOF while read initial buffer from " + path); + } + effectiveLength -= readLen; + digest.update(digestBuf, 0, readLen); + + hashValue = digest.digest(); + hashMatches = Arrays.equals(quickCheckHash, hashValue); + if (hashMatches) { + /* + * Need to re-initialize the digester due to the Javadoc: + * + * "The digest method can be called once for a given number + * of updates. After digest has been called, the MessageDigest + * object is reset to its initialized state." + */ + if (effectiveLength > 0L) { + digest = BuiltinDigests.md5.create(); + digest.init(); + digest.update(digestBuf, 0, readLen); + hashValue = null; // start again + } + } else { + if (log.isTraceEnabled()) { + log.trace("doMD5Hash({})({}) offset={}, length={} - quick-hash mismatched expected={}, actual={}", + getServerSession(), path, startOffset, length, + BufferUtils.toHex(':', quickCheckHash), + BufferUtils.toHex(':', hashValue)); + } + } + } + + if (hashMatches) { + while (effectiveLength > 0L) { + int remainLen = Math.min(digestBuf.length, (int) effectiveLength); + ByteBuffer bb = wb; + if (remainLen < digestBuf.length) { + bb = ByteBuffer.wrap(digestBuf, 0, remainLen); + } + bb.clear(); // prepare for next read + + int readLen = channel.read(bb); + if (readLen < 0) { + break; // user may have specified more than we have available + } + effectiveLength -= readLen; + digest.update(digestBuf, 0, readLen); + } + + if (hashValue == null) { // check if did any more iterations after the quick hash + hashValue = digest.digest(); + } + } else { + hashValue = GenericUtils.EMPTY_BYTE_ARRAY; + } + } + + if (log.isTraceEnabled()) { + log.trace("doMD5Hash({})({}) offset={}, length={} - matches={}, quick={} hash={}", + getServerSession(), path, startOffset, length, hashMatches, + BufferUtils.toHex(':', quickCheckHash), + BufferUtils.toHex(':', hashValue)); + } + + return hashValue; + } + + protected abstract void doCheckFileHash( + int id, String targetType, String target, Collection<String> algos, + long startOffset, long length, int blockSize, Buffer buffer) + throws Exception; + + protected void doReadLink(Buffer buffer, int id) throws IOException { + String path = buffer.getString(); + String l; + try { + if (log.isDebugEnabled()) { + log.debug("doReadLink({})[id={}] SSH_FXP_READLINK path={}", + getServerSession(), id, path); + } + l = doReadLink(id, path); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_READLINK, path); + return; + } + + sendLink(BufferUtils.clear(buffer), id, l); + } + + protected String doReadLink(int id, String path) throws IOException { + Path f = resolveFile(path); + Path t = Files.readSymbolicLink(f); + if (log.isDebugEnabled()) { + log.debug("doReadLink({})[id={}] path={}[{}]: {}", + getServerSession(), id, path, f, t); + } + return t.toString(); + } + + protected void doRename(Buffer buffer, int id) throws IOException { + String oldPath = buffer.getString(); + String newPath = buffer.getString(); + int flags = 0; + int version = getVersion(); + if (version >= SftpConstants.SFTP_V5) { + flags = buffer.getInt(); + } + try { + doRename(id, oldPath, newPath, flags); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_RENAME, oldPath, newPath, flags); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected void doRename(int id, String oldPath, String newPath, int flags) throws IOException { + if (log.isDebugEnabled()) { + log.debug("doRename({})[id={}] SSH_FXP_RENAME (oldPath={}, newPath={}, flags=0x{})", + getServerSession(), id, oldPath, newPath, Integer.toHexString(flags)); + } + + Collection<CopyOption> opts = Collections.emptyList(); + if (flags != 0) { + opts = new ArrayList<>(); + if ((flags & SftpConstants.SSH_FXP_RENAME_ATOMIC) == SftpConstants.SSH_FXP_RENAME_ATOMIC) { + opts.add(StandardCopyOption.ATOMIC_MOVE); + } + if ((flags & SftpConstants.SSH_FXP_RENAME_OVERWRITE) == SftpConstants.SSH_FXP_RENAME_OVERWRITE) { + opts.add(StandardCopyOption.REPLACE_EXISTING); + } + } + + doRename(id, oldPath, newPath, opts); + } + + protected void doRename(int id, String oldPath, String newPath, Collection<CopyOption> opts) throws IOException { + Path o = resolveFile(oldPath); + Path n = resolveFile(newPath); + SftpEventListener listener = getSftpEventListenerProxy(); + ServerSession session = getServerSession(); + + listener.moving(session, o, n, opts); + try { + Files.move(o, n, GenericUtils.isEmpty(opts) ? IoUtils.EMPTY_COPY_OPTIONS : opts.toArray(new CopyOption[opts.size()])); + } catch (IOException | RuntimeException e) { + listener.moved(session, o, n, opts, e); + throw e; + } + listener.moved(session, o, n, opts, null); + } + + // see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-extensions-00#section-7 + protected void doCopyData(Buffer buffer, int id) throws IOException { + String readHandle = buffer.getString(); + long readOffset = buffer.getLong(); + long readLength = buffer.getLong(); + String writeHandle = buffer.getString(); + long writeOffset = buffer.getLong(); + try { + doCopyData(id, readHandle, readOffset, readLength, writeHandle, writeOffset); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, + SftpConstants.SSH_FXP_EXTENDED, SftpConstants.EXT_COPY_DATA, + readHandle, readOffset, readLength, writeHandle, writeOffset); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected abstract void doCopyData(int id, String readHandle, long readOffset, long readLength, String writeHandle, long writeOffset) throws IOException; + + // see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-extensions-00#section-6 + protected void doCopyFile(Buffer buffer, int id) throws IOException { + String srcFile = buffer.getString(); + String dstFile = buffer.getString(); + boolean overwriteDestination = buffer.getBoolean(); + + try { + doCopyFile(id, srcFile, dstFile, overwriteDestination); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, + SftpConstants.SSH_FXP_EXTENDED, SftpConstants.EXT_COPY_FILE, srcFile, dstFile, overwriteDestination); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected void doCopyFile(int id, String srcFile, String dstFile, boolean overwriteDestination) throws IOException { + if (log.isDebugEnabled()) { + log.debug("doCopyFile({})[id={}] SSH_FXP_EXTENDED[{}] (src={}, dst={}, overwrite=0x{})", + getServerSession(), id, SftpConstants.EXT_COPY_FILE, + srcFile, dstFile, overwriteDestination); + } + + doCopyFile(id, srcFile, dstFile, + overwriteDestination + ? Collections.singletonList(StandardCopyOption.REPLACE_EXISTING) + : Collections.emptyList()); + } + + protected void doCopyFile(int id, String srcFile, String dstFile, Collection<CopyOption> opts) throws IOException { + Path src = resolveFile(srcFile); + Path dst = resolveFile(dstFile); + Files.copy(src, dst, GenericUtils.isEmpty(opts) ? IoUtils.EMPTY_COPY_OPTIONS : opts.toArray(new CopyOption[opts.size()])); + } + + protected void doBlock(Buffer buffer, int id) throws IOException { + String handle = buffer.getString(); + long offset = buffer.getLong(); + long length = buffer.getLong(); + int mask = buffer.getInt(); + + try { + doBlock(id, handle, offset, length, mask); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_BLOCK, handle, offset, length, mask); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected abstract void doBlock(int id, String handle, long offset, long length, int mask) throws IOException; + + protected void doUnblock(Buffer buffer, int id) throws IOException { + String handle = buffer.getString(); + long offset = buffer.getLong(); + long length = buffer.getLong(); + try { + doUnblock(id, handle, offset, length); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_UNBLOCK, handle, offset, length); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected abstract void doUnblock(int id, String handle, long offset, long length) throws IOException; + + protected void doStat(Buffer buffer, int id) throws IOException { + String path = buffer.getString(); + int flags = SftpConstants.SSH_FILEXFER_ATTR_ALL; + int version = getVersion(); + if (version >= SftpConstants.SFTP_V4) { + flags = buffer.getInt(); + } + + Map<String, Object> attrs; + try { + attrs = doStat(id, path, flags); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_STAT, path, flags); + return; + } + + sendAttrs(BufferUtils.clear(buffer), id, attrs); + } + + protected Map<String, Object> doStat(int id, String path, int flags) throws IOException { + if (log.isDebugEnabled()) { + log.debug("doStat({})[id={}] SSH_FXP_STAT (path={}, flags=0x{})", + getServerSession(), id, path, Integer.toHexString(flags)); + } + + /* + * SSH_FXP_STAT and SSH_FXP_LSTAT only differ in that SSH_FXP_STAT + * follows symbolic links on the server, whereas SSH_FXP_LSTAT does not. + */ + Path p = resolveFile(path); + return resolveFileAttributes(p, flags, IoUtils.getLinkOptions(true)); + } + + protected void doRealPath(Buffer buffer, int id) throws IOException { + String path = buffer.getString(); + if (log.isDebugEnabled()) { + log.debug("doRealPath({})[id={}] SSH_FXP_REALPATH (path={})", getServerSession(), id, path); + } + path = GenericUtils.trimToEmpty(path); + if (GenericUtils.isEmpty(path)) { + path = "."; + } + + Map<String, ?> attrs = Collections.emptyMap(); + Pair<Path, Boolean> result; + try { + int version = getVersion(); + if (version < SftpConstants.SFTP_V6) { + /* + * See http://www.openssh.com/txt/draft-ietf-secsh-filexfer-02.txt: + * + * The SSH_FXP_REALPATH request can be used to have the server + * canonicalize any given path name to an absolute path. + * + * See also SSHD-294 + */ + Path p = resolveFile(path); + LinkOption[] options = + getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p); + result = doRealPathV345(id, path, p, options); + } else { + /* + * See https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.9 + * + * This field is optional, and if it is not present in the packet, it + * is assumed to be SSH_FXP_REALPATH_NO_CHECK. + */ + int control = SftpConstants.SSH_FXP_REALPATH_NO_CHECK; + if (buffer.available() > 0) { + control = buffer.getUByte(); + if (log.isDebugEnabled()) { + log.debug("doRealPath({}) - control=0x{} for path={}", + getServerSession(), Integer.toHexString(control), path); + } + } + + Collection<String> extraPaths = new LinkedList<>(); + while (buffer.available() > 0) { + extraPaths.add(buffer.getString()); + } + + Path p = resolveFile(path); + LinkOption[] options = + getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p); + result = doRealPathV6(id, path, extraPaths, p, options); + + p = result.getFirst(); + options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p); + Boolean status = result.getSecond(); + switch (control) { + case SftpConstants.SSH_FXP_REALPATH_STAT_IF: + if (status == null) { + attrs = handleUnknownStatusFileAttributes(p, SftpConstants.SSH_FILEXFER_ATTR_ALL, options); + } else if (status) { + try { + attrs = getAttributes(p, options); + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug("doRealPath({}) - failed ({}) to retrieve attributes of {}: {}", + getServerSession(), e.getClass().getSimpleName(), p, e.getMessage()); + } + if (log.isTraceEnabled()) { + log.trace("doRealPath(" + getServerSession() + ")[" + p + "] attributes retrieval failure details", e); + } + } + } else { + if (log.isDebugEnabled()) { + log.debug("doRealPath({}) - dummy attributes for non-existing file: {}", getServerSession(), p); + } + } + break; + case SftpConstants.SSH_FXP_REALPATH_STAT_ALWAYS: + if (status == null) { + attrs = handleUnknownStatusFileAttributes(p, SftpConstants.SSH_FILEXFER_ATTR_ALL, options); + } else if (status) { + attrs = getAttributes(p, options); + } else { + throw new NoSuchFileException(p.toString(), p.toString(), "Real path N/A for target"); + } + break; + case SftpConstants.SSH_FXP_REALPATH_NO_CHECK: + break; + default: + log.warn("doRealPath({}) unknown control value 0x{} for path={}", + getServerSession(), Integer.toHexString(control), p); + } + } + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_REALPATH, path); + return; + } + + sendPath(BufferUtils.clear(buffer), id, result.getFirst(), attrs); + } + + protected Pair<Path, Boolean> doRealPathV6( + int id, String path, Collection<String> extraPaths, Path p, LinkOption... options) throws IOException { + int numExtra = GenericUtils.size(extraPaths); + if (numExtra > 0) { + if (log.isDebugEnabled()) { + log.debug("doRealPathV6({})[id={}] path={}, extra={}", + getServerSession(), id, path, extraPaths); + } + StringBuilder sb = new StringBuilder(GenericUtils.length(path) + numExtra * 8); + sb.append(path); + + for (String p2 : extraPaths) { + p = p.resolve(p2); + options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p); + sb.append('/').append(p2); + } + + path = sb.toString(); + } + + return validateRealPath(id, path, p, options); + } + + protected Pair<Path, Boolean> doRealPathV345(int id, String path, Path p, LinkOption... options) throws IOException { + return validateRealPath(id, path, p, options); + } + + /** + * @param id The request identifier + * @param path The original path + * @param f The resolve {@link Path} + * @param options The {@link LinkOption}s to use to verify file existence and access + * @return A {@link Pair} whose left-hand is the <U>absolute <B>normalized</B></U> + * {@link Path} and right-hand is a {@link Boolean} indicating its status + * @throws IOException If failed to validate the file + * @see IoUtils#checkFileExists(Path, LinkOption...) + */ + protected Pair<Path, Boolean> validateRealPath(int id, String path, Path f, LinkOption... options) throws IOException { + Path p = normalize(f); + Boolean status = IoUtils.checkFileExists(p, options); + return new Pair<>(p, status); + } + + protected void doRemoveDirectory(Buffer buffer, int id) throws IOException { + String path = buffer.getString(); + try { + doRemoveDirectory(id, path, IoUtils.getLinkOptions(false)); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_RMDIR, path); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected void doRemoveDirectory(int id, String path, LinkOption... options) throws IOException { + Path p = resolveFile(path); + if (log.isDebugEnabled()) { + log.debug("doRemoveDirectory({})[id={}] SSH_FXP_RMDIR (path={})[{}]", + getServerSession(), id, path, p); + } + if (Files.isDirectory(p, options)) { + doRemove(id, p); + } else { + throw new NotDirectoryException(p.toString()); + } + } + + /** + * Called when need to delete a file / directory - also informs the {@link SftpEventListener} + * + * @param id Deletion request ID + * @param p {@link Path} to delete + * @throws IOException If failed to delete + */ + protected void doRemove(int id, Path p) throws IOException { + SftpEventListener listener = getSftpEventListenerProxy(); + ServerSession session = getServerSession(); + listener.removing(session, p); + try { + Files.delete(p); + } catch (IOException | RuntimeException e) { + listener.removed(session, p, e); + throw e; + } + listener.removed(session, p, null); + } + + protected void doMakeDirectory(Buffer buffer, int id) throws IOException { + String path = buffer.getString(); + Map<String, ?> attrs = readAttrs(buffer); + try { + doMakeDirectory(id, path, attrs, IoUtils.getLinkOptions(false)); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_MKDIR, path, attrs); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected void doMakeDirectory(int id, String path, Map<String, ?> attrs, LinkOption... options) throws IOException { + Path p = resolveFile(path); + if (log.isDebugEnabled()) { + log.debug("doMakeDirectory({})[id={}] SSH_FXP_MKDIR (path={}[{}], attrs={})", + getServerSession(), id, path, p, attrs); + } + + Boolean status = IoUtils.checkFileExists(p, options); + if (status == null) { + throw new AccessDeniedException(p.toString(), p.toString(), "Cannot validate make-directory existence"); + } + + if (status) { + if (Files.isDirectory(p, options)) { + throw new FileAlreadyExistsException(p.toString(), p.toString(), "Target directory already exists"); + } else { + throw new FileAlreadyExistsException(p.toString(), p.toString(), "Already exists as a file"); + } + } else { + SftpEventListener listener = getSftpEventListenerProxy(); + ServerSession session = getServerSession(); + listener.creating(session, p, attrs); + try { + Files.createDirectory(p); + doSetAttributes(p, attrs); + } catch (IOException | RuntimeException e) { + listener.created(session, p, attrs, e); + throw e; + } + listener.created(session, p, attrs, null); + } + } + + protected void doRemove(Buffer buffer, int id) throws IOException { + String path = buffer.getString(); + try { + /* + * If 'filename' is a symbolic link, the link is removed, + * not the file it points to. + */ + doRemove(id, path, IoUtils.getLinkOptions(false)); + } catch (IOException | RuntimeException e) { + sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_REMOVE, path); + return; + } + + sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); + } + + protected void doRemove(int id, String path, LinkOption... options) throws IOException { + Path p = resolveFile(path); + if (log.isDebugEnabled()) { + log.debug("doRemove({})[id={}] SSH_FXP_REMOVE (path={}[{}])", + getServerSession(), id, path, p); + } + + Boolean status = IoUtils.checkFileExists(p, options); + if (status == null) { + throw new AccessDeniedException(p.toString(), p.toString(), "Cannot determine existence of remove candidate"); + } + if (!status) { + throw new NoSuchFileException(p.toString(), p.toString(), "Removal candidate not found"); + } else if (Files.isDirectory(p, options)) { + throw new SftpException(SftpConstants.SSH_FX_FILE_IS_A_DIRECTORY, p.toString() + " is a folder"); + } else { + doRemove(id, p); + } + } + + protected void doExtended(Buffer buffer, int id) throws IOException { + executeExtendedCommand(buffer, id, buffer.getString()); + } + + /** + * @param buffer The command {@link Buffer} + * @param id The request id + * @param extension The extension name + * @throws IOException If failed to execute the extension + */ + protected abstract void executeExtendedCommand(Buffer buffer, int id, String extension) throws IOException; + + protected void appendExtensions(Buffer buffer, String supportedVersions) { + appendVersionsExtension(buffer, supportedVersions); + appendNewlineExtension(buffer, resolveNewlineValue(getServerSession())); + appendVendorIdExtension(buffer, VersionProperties.getVersionProperties()); + appendOpenSSHExtensions(buffer); + appendAclSupportedExtension(buffer); + + Map<String, OptionalFeature> extensions = getSupportedClientExtensions(); + int numExtensions = GenericUtils.size(extensions); + List<String> extras = (numExtensions <= 0) ? Collections.emptyList() : new ArrayList<>(numExtensions); + if (numExtensions > 0) { + ServerSession session = getServerSession(); + extensions.forEach((name, f) -> { + if (!f.isSupported()) { + if (log.isDebugEnabled()) { + log.debug("appendExtensions({}) skip unsupported extension={}", session, name); + } + return; + } + + extras.add(name); + }); + } + appendSupportedExtension(buffer, extras); + appendSupported2Extension(buffer, extras); + } + + protected int appendAclSupportedExtension(Buffer buffer) { + ServerSession session = getServerSession(); + Collection<Integer> maskValues = resolveAclSupportedCapabilities(session); + int mask = AclSupportedParser.AclCapabilities.constructAclCapabilities(maskValues); + if (mask != 0) { + if (log.isTraceEnabled()) { + log.trace("appendAclSupportedExtension({}) capabilities={}", + session, AclSupportedParser.AclCapabilities.decodeAclCapabilities(mask)); + } + + buffer.putString(SftpConstants.EXT_ACL_SUPPORTED); + + // placeholder for length + int lenPos = buffer.wpos(); + buffer.putInt(0); + buffer.putInt(mask); + BufferUtils.updateLengthPlaceholder(buffer, lenPos); + } + + return mask; + } + + protected Collection<Integer> resolveAclSupportedCapabilities(ServerSession session) { + String override = session.getString(ACL_SUPPORTED_MASK_PROP); + if (override == null) { + return DEFAULT_ACL_SUPPORTED_MASK; + } + + // empty means not supported + if (log.isDebugEnabled()) { + log.debug("resolveAclSupportedCapabilities({}) override='{}'", session, override); + } + + if (override.length() == 0) { + return Collections.emptySet(); + } + + String[] names = GenericUtils.split(override, ','); + Set<Integer> maskValues = new HashSet<>(names.length); + for (String n : names) { + Integer v = ValidateUtils.checkNotNull( + AclSupportedParser.AclCapabilities.getAclCapabilityValue(n), "Unknown ACL capability: %s", n); + maskValues.add(v); + } + + return maskValues; + } + + protected List<OpenSSHExtension> appendOpenSSHExtensions(Buffer buffer) { + List<OpenSSHExtension> extList = resolveOpenSSHExtensions(getServerSession()); + if (GenericUtils.isEmpty(extList)) { + return extList; + } + + for (OpenSSHExtension ext : extList) { + buffer.putString(ext.getName()); + buffer.putString(ext.getVersion()); + } + + return extList; + } + + protected List<OpenSSHExtension> resolveOpenSSHExtensions(ServerSession session) { + String value = session.getString(OPENSSH_EXTENSIONS_PROP); + if (value == null) { // No override + return DEFAULT_OPEN_SSH_EXTENSIONS; + } + + if (log.isDebugEnabled()) { + log.debug("resolveOpenSSHExtensions({}) override='{}'", session, value); + } + + String[] pairs = GenericUtils.split(value, ','); + int numExts = GenericUtils.length(pairs); + if (numExts <= 0) { // User does not want to report ANY extensions + return Collections.emptyList(); + } + + List<OpenSSHExtension> extList = new ArrayList<>(numExts); + for (String nvp : pairs) { + nvp = GenericUtils.trimToEmpty(nvp); + if (GenericUtils.isEmpty(nvp)) { + continue; + } + + int pos = nvp.indexOf('='); + ValidateUtils.checkTrue((pos > 0) && (pos < (nvp.length() - 1)), "Malformed OpenSSH extension spec: %s", nvp); + String name = GenericUtils.trimToEmpty(nvp.substring(0, pos)); + String version = GenericUtils.trimToEmpty(nvp.substring(pos + 1)); + extList.add(new OpenSSHExtension(name, ValidateUtils.checkNotNullAndNotEmpty(version, "No version specified for OpenSSH extension %s", name))); + } + + return extList; + } + + protected Map<String, OptionalFeature> getSupportedClientExtensions() { + ServerSession session = getServerSession(); + String value = session.getString(CLIENT_EXTENSIONS_PROP); + if (value == null) { + return DEFAULT_SUPPORTED_CLIENT_EXTENSIONS; + } + + if (log.isDebugEnabled()) { + log.debug("getSupportedClientExtensions({}) override='{}'", session, value); + } + + if (value.length() <= 0) { // means don't report any extensions + return Collections.emptyMap(); + } + + if (value.indexOf(',') <= 0) { + return Collections.singletonMap(value, OptionalFeature.TRUE); + } + + String[] comps = GenericUtils.split(value, ','); + Map<String, OptionalFeature> result = new LinkedHashMap<>(comps.length); + for (String c : comps) { + result.put(c, OptionalFeature.TRUE); + } + + return result; + } + + /** + * Appends the "versions" extension to the buffer. <B>Note:</B> + * if overriding this method make sure you either do not append anything + * or use the correct extension name + * + * @param buffer The {@link Buffer} to append to + * @param value The recommended value - ignored if {@code null}/empty + * @see SftpConstants#EXT_VERSIONS + */ + protected void appendVersionsExtension(Buffer buffer, String value) { + if (GenericUtils.isEmpty(value)) { + return; + } + + if (log.isDebugEnabled()) { + log.debug("appendVersionsExtension({}) value={}", getServerSession(), value); + } + + buffer.putString(SftpConstants.EXT_VERSIONS); + buffer.putString(value); + } + + /** + * Appends the "newline" extension to the buffer. <B>Note:</B> + * if overriding this method make sure you either do not append anything + * or use the correct extension name + * + * @param buffer The {@link Buffer} to append to + * @param value The recommended value - ignored if {@code null}/empty + * @see SftpConstants#EXT_NEWLINE + */ + protected void appendNewlineExtension(Buffer buffer, String value) { + if (GenericUtils.isEmpty(value)) { + return; + } + + if (log.isDebugEnabled()) { + log.debug("appendNewlineExtension({}) value={}", + getServerSession(), BufferUtils.toHex(':', value.getBytes(StandardCharsets.UTF_8))); + } + + buffer.putString(SftpConstants.EXT_NEWLINE); + buffer.putString(value); + } + + protected String resolveNewlineValue(ServerSession session) { + String value = session.getString(NEWLINE_VALUE); + if (value == null) { + return IoUtils.EOL; + } else { + return value; // empty means disabled + } + } + + /** + * Appends the "vendor-id" extension to the buffer. <B>Note:</B> + * if overriding this method make sure you either do not append anything + * or use the correct extension name + * + * @param buffer The {@link Buffer} to append to + * @param versionProperties The currently available version properties - ignored + * if {@code null}/empty. The code expects the following values: + * <UL> + * <LI>{@code groupId} - as the vendor name</LI> + * <LI>{@code artifactId} - as the product name</LI> + * <LI>{@code version} - as the product version</LI> + * </UL> + * @see SftpConstants#EXT_VENDOR_ID + * @see <A HREF="http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt">DRAFT 09 - section 4.4</A> + */ + protected void appendVendorIdExtension(Buffer buffer, Map<String, ?> versionProperties) { + if (GenericUtils.isEmpty(versionProperties)) { + return; + } + + if (log.isDebugEnabled()) { + log.debug("appendVendorIdExtension({}): {}", getServerSession(), versionProperties); + } + buffer.putString(SftpConstants.EXT_VENDOR_ID); + + PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(Collections.unmodifiableMap(versionProperties)); + // placeholder for length + int lenPos = buffer.wpos(); + buffer.putInt(0); + buffer.putString(resolver.getStringProperty("groupId", getClass().getPackage().getName())); // vendor-name + buffer.putString(resolver.getStringProperty("artifactId", getClass().getSimpleName())); // product-name + buffer.putString(resolver.getStringProperty("version", FactoryManager.DEFAULT_VERSION)); // product-version + buffer.putLong(0L); // product-build-number + BufferUtils.updateLengthPlaceholder(buffer, lenPos); + } + + /** + * Appends the "supported" extension to the buffer. <B>Note:</B> + * if overriding this method make sure you either do not append anything + * or use the correct extension name + * + * @param buffer The {@link Buffer} to append to + * @param extras The extra extensions that are available and can be reported + * - may be {@code null}/empty + */ + protected void appendSupportedExtension(Buffer buffer, Collection<String> extras) { + buffer.putString(SftpConstants.EXT_SUPPORTED); + + int lenPos = buffer.wpos(); + buffer.putInt(0); // length placeholder + // supported-attribute-mask + buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_SIZE | SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS + | SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME | SftpConstants.SSH_FILEXFER_ATTR_CREATETIME + | SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME | SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP + | SftpConstants.SSH_FILEXFER_ATTR_BITS); + // TODO: supported-attribute-bits + buffer.putInt(0); + // supported-open-flags + buffer.putInt(SftpConstants.SSH_FXF_READ | SftpConstants.SSH_FXF_WRITE | SftpConstants.SSH_FXF_APPEND + | SftpConstants.SSH_FXF_CREAT | SftpConstants.SSH_FXF_TRUNC | SftpConstants.SSH_FXF_EXCL); + // TODO: supported-access-mask + buffer.putInt(0); + // max-read-size + buffer.putInt(0); + // supported extensions + buffer.putStringList(extras, false); + + BufferUtils.updateLengthPlaceholder(buffer, lenPos); + } + + /** + * Appends the "supported2" extension to the buffer. <B>Note:</B> + * if overriding this method make sure you either do not append anything + * or use the correct extension name + * + * @param buffer The {@link Buffer} to append to + * @param extras The extra extensions that are available and can be reported + * - may be {@code null}/empty + * @see SftpConstants#EXT_SUPPORTED + * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-10">DRAFT 13 section 5.4</A> + */ + protected void appendSupported2Extension(Buffer buffer, Collection<String> extras) { + buffer.putString(SftpConstants.EXT_SUPPORTED2); + + int lenPos = buffer.wpos(); + buffer.putInt(0); // length placeholder + // supported-attribute-mask + buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_SIZE | SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS + | SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME | SftpConstants.SSH_FILEXFER_ATTR_CREATETIME + | SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME | SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP + | SftpConstants.SSH_FILEXFER_ATTR_BITS); + // TODO: supported-attribute-bits + buffer.putInt(0); + // supported-open-flags + buffer.putInt(SftpConstants.SSH_FXF_ACCESS_DISPOSITION | SftpConstants.SSH_FXF_APPEND_DATA); + // TODO: supported-access-mask + buffer.putInt(0); + // max-read-size + buffer.putInt(0); + // supported-open-block-vector + buffer.putShort(0); + // supported-block-vector + buffer.putShort(0); + // attrib-extension-count + attributes name + buffer.putStringList(Collections.<String>emptyList(), true); + // extension-count + supported extensions + buffer.putStringList(extras, true); + + BufferUtils.updateLengthPlaceholder(buffer, lenPos); + } + + protected void sendHandle(Buffer buffer, int id, String handle) throws IOException { + buffer.putByte((byte) SftpConstants.SSH_FXP_HANDLE); + buffer.putInt(id); + buffer.putString(handle); + send(buffer); + } + + protected void sendAttrs(Buffer buffer, int id, Map<String, ?> attributes) throws IOException { + buffer.putByte((byte) SftpConstants.SSH_FXP_ATTRS); + buffer.putInt(id); + writeAttrs(buffer, attributes); + send(buffer); + } + + protected void sendLink(Buffer buffer, int id, String link) throws IOException { + //in case we are running on Windows + String unixPath = link.replace(File.separatorChar, '/'); + //normalize the given path, use *nix style separator + String normalizedPath = SelectorUtils.normalizePath(unixPath, "/"); + + buffer.putByte((byte) SftpConstants.SSH_FXP_NAME); + buffer.putInt(id); + buffer.putInt(1); // one response + buffer.putString(normalizedPath); + + /* + * As per the spec (https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.10): + * + * The server will respond with a SSH_FXP_NAME packet containing only + * one name and a dummy attributes value. + */ + Map<String, Object> attrs = Collections.emptyMap(); + int version = getVersion(); + if (version == SftpConstants.SFTP_V3) { + buffer.putString(SftpHelper.getLongName(normalizedPath, attrs)); + } + + writeAttrs(buffer, attrs); + SftpHelper.indicateEndOfNamesList(buffer, getVersion(), getServerSession()); + send(buffer); + } + + protected void sendPath(Buffer buffer, int id, Path f, Map<String, ?> attrs) throws IOException { + buffer.putByte((byte) SftpConstants.SSH_FXP_NAME); + buffer.putInt(id); + buffer.putInt(1); // one reply + + String originalPath = f.toString(); + //in case we are running on Windows + String unixPath = originalPath.replace(File.separatorChar, '/'); + //normalize the given path, use *nix style separator + String normalizedPath = SelectorUtils.normalizePath(unixPath, "/"); + if (normalizedPath.length() == 0) { + normalizedPath = "/"; + } + buffer.putString(normalizedPath); + + int version = getVersion(); + if (version == SftpConstants.SFTP_V3) { + f = resolveFile(normalizedPath); + buffer.putString(getLongName(f, getShortName(f), attrs)); + } + + writeAttrs(buffer, attrs); + SftpHelper.indicateEndOfNamesList(buffer, getVersion(), getServerSession()); + send(buffer); + } + + /** + * @param id Request id + * @param handle The (opaque) handle assigned to this directory + * @param dir The {@link DirectoryHandle} + * @param buffer The {@link Buffer} to write the results + * @param maxSize Max. buffer size + * @param options The {@link LinkOption}-s to use when querying the directory contents + * @return Number of written entries + * @throws IOException If failed to generate an entry + */ + protected int doReadDir( + int id, String handle, DirectoryHandle dir, Buffer buffer, int maxSize, LinkOption... options) throws IOException { + int nb = 0; + Map<String, Path> entries = new TreeMap<>(Comparator.naturalOrder()); + while ((dir.isSendDot() || dir.isSendDotDot() || dir.hasNext()) && (buffer.wpos() < maxSize)) { + if (dir.isSendDot()) { + writeDirEntry(id, dir, entries, buffer, nb, dir.getFile(), ".", options); + dir.markDotSent(); // do not send it again + } else if (dir.isSendDotDot()) { + Path dirPath = dir.getFile(); + writeDirEntry(id, dir, entries, buffer, nb, dirPath.getParent(), "..", options); + dir.markDotDotSent(); // do not send it again + } else { + Path f = dir.next(); + writeDirEntry(id, dir, entries, buffer, nb, f, getShortName(f), options); + } + + nb++; + } + + SftpEventListener listener = getSftpEventListenerProxy(); + listener.read(getServerSession(), handle, dir, entries); + return nb; + } + + /** + * @param id Request id + * @param dir The {@link DirectoryHandle} + * @param entries An in / out {@link Map} for updating the written entry - + * key = short name, value = entry {@link Path} + * @param buffer The {@link Buffer} to write the results + * @param index Zero-based index of the entry to be written + * @param f The entry {@link Path} + * @param shortName The entry short name + * @param options The {@link LinkOption}s to use for querying the entry-s attributes + * @throws IOException If failed to generate the entry data + */ + protected void writeDirEntry( + int id, DirectoryHandle dir, Map<String, Path> entries, Buffer buffer, int index, Path f, String shortName, LinkOption... options) + throws IOException { + Map<String, ?> attrs = resolveFileAttributes(f, SftpConstants.SSH_FILEXFER_ATTR_ALL, options); + entries.put(shortName, f); + + buffer.putString(shortName); + int version = getVersion(); + if (version == SftpConstants.SFTP_V3) { + String longName = getLongName(f, shortName, options); + buffer.putString(longName); + if (log.isTraceEnabled()) { + log.trace("writeDirEntry(" + getServerSession() + ") id=" + id + ")[" + index + "] - " + + shortName + " [" + longName + "]: " + attrs); + } + } else { + if (log.isTraceEnabled()) { + log.trace("writeDirEntry(" + getServerSession() + "(id=" + id + ")[" + index + "] - " + + shortName + ": " + attrs); + } + } + + writeAttrs(buffer, attrs); + } + + protected String getLongName(Path f, String shortName, LinkOption... options) throws IOException { + return getLongName(f, shortName, true, options); + } + + protected String getLongName(Path f, String shortName, boolean sendAttrs, LinkOption... options) throws IOException { + Map<String, Object> attributes; + if (sendAttrs) { + attributes = getAttributes(f, options); + } else { + attributes = Collections.emptyMap(); + } + return getLongName(f, shortName, attributes); + } + + protected String getLongName(Path f, String shortName, Map<String, ?> attributes) throws IOException { + return SftpHelper.getLongName(shortName, attributes); + } + + protected String getShortName(Path f) throws IOException { + Path nrm = normalize(f); + int count = nrm.getNameCount(); + /* + * According to the javadoc: + * + * The number of elements in the path, or 0 if this path only + * represents a root component + */ + if (OsUtils.isUNIX()) { + Path name = f.getFileName(); + if (name == null) { + Path p = resolveFile("."); + name = p.getFileName(); + } + + if (name == null) { + if (count > 0) { + name = nrm.getFileName(); + } + } + + if (name != null) { + return name.toString(); + } else { + return nrm.toString(); + } + } else { // need special handling for Windows root drives + if (count > 0) { + Path name = nrm.getFileName(); + return name.toString(); + } else { + return nrm.toString().replace(File.separatorChar, '/'); + } + } + } + + protected NavigableMap<String, Object> resolveFileAttributes(Path file, int flags, LinkOption... options) throws IOException { + Boolean status = IoUtils.checkFileExists(file, options); + if (status == null) { + return handleUnknownStatusFileAttributes(file, flags, options); + } else if (!status) { + throw new NoSuchFileException(file.toString(), file.toString(), "Attributes N/A for target"); + } else { + return getAttributes(file, flags, options); + } + } + + protected void writeAttrs(Buffer buffer, Map<String, ?> attributes) throws IOException { + SftpHelper.writeAttrs(buffer, getVersion(), attributes); + } + + protected NavigableMap<String, Object> getAttributes(Path file, LinkOption... options) throws IOException { + return getAttributes(file, SftpConstants.SSH_FILEXFER_ATTR_ALL, options); + } + + protected NavigableMap<String, Object> handleUnknownStatusFileAttributes(Path file, int flags, LinkOption... options) throws IOException { + UnsupportedAttributePolicy unsupportedAttributePolicy = getUnsupportedAttributePolicy(); + switch (unsupportedAttributePolicy) { + case Ignore: + break; + case ThrowException: + throw new AccessDeniedException(file.toString(), file.toString(), "Cannot determine existence for attributes of target"); + case Warn: + log.warn("handleUnknownStatusFileAttributes(" + getServerSession() + ")[" + file + "] cannot determine existence"); + break; + default: + log.warn("handleUnknownStatusFileAttributes(" + getServerSession() + ")[" + file + "] unknown policy: " + unsupportedAttributePolicy); + } + + return getAttributes(file, flags, options); + } + + /** + * @param file The {@link Path} location for the required attributes + * @param flags A mask of the original required attributes - ignored by the + * default implementation + * @param options The {@link LinkOption}s to use in order to access the file + * if necessary + * @return A {@link Map} of the retrieved attributes + * @throws IOException If failed to access the file + * @see #resolveMissingFileAttributes(Path, int, Map, LinkOption...) + */ + protected NavigableMap<String, Object> getAttributes(Path file, int flags, LinkOption... options) throws IOException { + FileSystem fs = file.getFileSystem(); + Collection<String> supportedViews = fs.supportedFileAttributeViews(); + NavigableMap<String, Object> attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER)
<TRUNCATED>