You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by gn...@apache.org on 2018/04/16 11:47:55 UTC
[08/30] mina-sshd git commit: [SSHD-815] Extract SFTP in its own
module
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java
new file mode 100644
index 0000000..08213ee
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java
@@ -0,0 +1,2580 @@
+/*
+ * 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.AbstractMap.SimpleImmutableEntry;
+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.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:dev@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();
+
+ boolean traceEnabled = log.isTraceEnabled();
+ 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 (traceEnabled) {
+ 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 (traceEnabled) {
+ 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();
+ boolean traceEnabled = log.isTraceEnabled();
+ 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 (traceEnabled) {
+ 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 (traceEnabled) {
+ 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();
+ boolean debugEnabled = log.isDebugEnabled();
+ if (debugEnabled) {
+ 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();
+ Map.Entry<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 (debugEnabled) {
+ 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.getKey();
+ options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p);
+ Boolean status = result.getValue();
+ 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 (debugEnabled) {
+ 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 (debugEnabled) {
+ 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.getKey(), attrs);
+ }
+
+ protected SimpleImmutableEntry<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 SimpleImmutableEntry<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 SimpleImmutableEntry} whose key is the <U>absolute <B>normalized</B></U>
+ * {@link Path} and value is a {@link Boolean} indicating its status
+ * @throws IOException If failed to validate the file
+ * @see IoUtils#checkFileExists(Path, LinkOption...)
+ */
+ protected SimpleImmutableEntry<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 SimpleImmutableEntry<>(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();
+ boolean debugEnabled = log.isDebugEnabled();
+ extensions.forEach((name, f) -> {
+ if (!f.isSupported()) {
+ if (debugEnabled) {
+ 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, '/');
+
+ buffer.putByte((byte) SftpConstants.SSH_FXP_NAME);
+ buffer.putInt(id);
+ buffer.putInt(1); // one response
+ buffer.putString(unixPath);
+
+ /*
+ * 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(unixPath, 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, '/');
+ buffer.putString(unixPath);
+
+ int version = getVersion();
+ if (version == SftpConstants.SFTP_V3) {
+ 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);
+ Collection<String> views;
+
+ if (GenericUtils.isEmpty(supportedViews)) {
+ views = Collections.emptyList();
+ } else if (supportedViews.contains("unix")) {
+ view
<TRUNCATED>