/* * 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.InputStream; import java.io.OutputStream; 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.FileSystemLoopException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.LinkOption; 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.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.IntUnaryOperator; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.sshd.common.Factory; 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.digest.DigestFactory; import org.apache.sshd.common.file.FileSystemAware; import org.apache.sshd.common.random.Random; 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.buffer.ByteArrayBuffer; 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.common.util.threads.ThreadUtils; import org.apache.sshd.server.Command; import org.apache.sshd.server.Environment; import org.apache.sshd.server.ExitCallback; import org.apache.sshd.server.SessionAware; import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.session.ServerSessionHolder; /** * SFTP subsystem * * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> */ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runnable, SessionAware, FileSystemAware, ServerSessionHolder, SftpEventListenerManager { /** * Properties key for the maximum of available open handles per session. */ public static final String MAX_OPEN_HANDLES_PER_SESSION = "max-open-handles-per-session"; public static final int DEFAULT_MAX_OPEN_HANDLES = Integer.MAX_VALUE; /** * Size in bytes of the opaque handle value * * @see #DEFAULT_FILE_HANDLE_SIZE */ public static final String FILE_HANDLE_SIZE = "sftp-handle-size"; public static final int MIN_FILE_HANDLE_SIZE = 4; // ~uint32 public static final int DEFAULT_FILE_HANDLE_SIZE = 16; public static final int MAX_FILE_HANDLE_SIZE = 64; // ~sha512 /** * Max. rounds to attempt to create a unique file handle - if all handles * already in use after these many rounds, then an exception is thrown * * @see #generateFileHandle(Path) * @see #DEFAULT_FILE_HANDLE_ROUNDS */ public static final String MAX_FILE_HANDLE_RAND_ROUNDS = "sftp-handle-rand-max-rounds"; public static final int MIN_FILE_HANDLE_ROUNDS = 1; public static final int DEFAULT_FILE_HANDLE_ROUNDS = MIN_FILE_HANDLE_SIZE; public static final int MAX_FILE_HANDLE_ROUNDS = MAX_FILE_HANDLE_SIZE; /** * Force the use of a given sftp version */ public static final String SFTP_VERSION = "sftp-version"; public static final int LOWER_SFTP_IMPL = SftpConstants.SFTP_V3; // Working implementation from v3 public static final int HIGHER_SFTP_IMPL = SftpConstants.SFTP_V6; // .. up to and including public static final String ALL_SFTP_IMPL = IntStream.rangeClosed(LOWER_SFTP_IMPL, HIGHER_SFTP_IMPL) .mapToObj(Integer::toString) .collect(Collectors.joining(",")); /** * 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; /** * Maximum amount of data allocated for listing the contents of a directory * in any single invocation of {@link #doReadDir(Buffer, int)} * * @see #DEFAULT_MAX_READDIR_DATA_SIZE */ public static final String MAX_READDIR_DATA_SIZE_PROP = "sftp-max-readdir-data-size"; public static final int DEFAULT_MAX_READDIR_DATA_SIZE = 16 * 1024; /** * 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)); public static final List<String> DEFAULT_UNIX_VIEW = Collections.singletonList("unix:*"); /** * 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"; /** * A {@link Map} of {@link FileInfoExtractor}s to be used to complete * attributes that are deemed important enough to warrant an extra * effort if not accessible via the file system attributes views */ public static final Map<String, FileInfoExtractor<?>> FILEATTRS_RESOLVERS = GenericUtils.<String, FileInfoExtractor<?>>mapBuilder(String.CASE_INSENSITIVE_ORDER) .put("isRegularFile", FileInfoExtractor.ISREG) .put("isDirectory", FileInfoExtractor.ISDIR) .put("isSymbolicLink", FileInfoExtractor.ISSYMLINK) .put("permissions", FileInfoExtractor.PERMISSIONS) .put("size", FileInfoExtractor.SIZE) .put("lastModifiedTime", FileInfoExtractor.LASTMODIFIED) .immutable(); /** * 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; protected ExitCallback callback; protected InputStream in; protected OutputStream out; protected OutputStream err; protected Environment env; protected Random randomizer; protected int fileHandleSize = DEFAULT_FILE_HANDLE_SIZE; protected int maxFileHandleRounds = DEFAULT_FILE_HANDLE_ROUNDS; protected ExecutorService executors; protected boolean shutdownExecutor; protected Future<?> pendingFuture; protected byte[] workBuf = new byte[Math.max(DEFAULT_FILE_HANDLE_SIZE, Integer.BYTES)]; protected FileSystem fileSystem = FileSystems.getDefault(); protected Path defaultDir = fileSystem.getPath(System.getProperty("user.dir")); protected long requestsCount; protected int version; protected final Map<String, byte[]> extensions = new HashMap<>(); protected final Map<String, Handle> handles = new HashMap<>(); protected final UnsupportedAttributePolicy unsupportedAttributePolicy; private ServerSession serverSession; private final AtomicBoolean closed = new AtomicBoolean(false); private final Collection<SftpEventListener> sftpEventListeners = new CopyOnWriteArraySet<>(); private final SftpEventListener sftpEventListenerProxy; private final SftpFileSystemAccessor fileSystemAccessor; /** * @param executorService The {@link ExecutorService} to be used by * the {@link SftpSubsystem} command when starting execution. If * {@code null} then a single-threaded ad-hoc service is used. * @param shutdownOnExit If {@code true} the {@link ExecutorService#shutdownNow()} * will be called when subsystem terminates - unless it is the ad-hoc * service, which will be shutdown regardless * @param policy The {@link UnsupportedAttributePolicy} to use if failed to access * some local file attributes * @param accessor The {@link SftpFileSystemAccessor} to use for opening files and directories * @see ThreadUtils#newSingleThreadExecutor(String) */ public SftpSubsystem(ExecutorService executorService, boolean shutdownOnExit, UnsupportedAttributePolicy policy, SftpFileSystemAccessor accessor) { if (executorService == null) { executors = ThreadUtils.newSingleThreadExecutor(getClass().getSimpleName()); shutdownExecutor = true; // we always close the ad-hoc executor service } else { executors = executorService; shutdownExecutor = shutdownOnExit; } unsupportedAttributePolicy = Objects.requireNonNull(policy, "No policy provided"); fileSystemAccessor = Objects.requireNonNull(accessor, "No accessor"); sftpEventListenerProxy = EventListenerUtils.proxyWrapper(SftpEventListener.class, getClass().getClassLoader(), sftpEventListeners); } public int getVersion() { return version; } public final UnsupportedAttributePolicy getUnsupportedAttributePolicy() { return unsupportedAttributePolicy; } public final 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)); } @Override public void setSession(ServerSession session) { this.serverSession = Objects.requireNonNull(session, "No session"); FactoryManager manager = session.getFactoryManager(); Factory<? extends Random> factory = manager.getRandomFactory(); this.randomizer = factory.create(); this.fileHandleSize = session.getIntProperty(FILE_HANDLE_SIZE, DEFAULT_FILE_HANDLE_SIZE); ValidateUtils.checkTrue(this.fileHandleSize >= MIN_FILE_HANDLE_SIZE, "File handle size too small: %d", this.fileHandleSize); ValidateUtils.checkTrue(this.fileHandleSize <= MAX_FILE_HANDLE_SIZE, "File handle size too big: %d", this.fileHandleSize); this.maxFileHandleRounds = session.getIntProperty(MAX_FILE_HANDLE_RAND_ROUNDS, DEFAULT_FILE_HANDLE_ROUNDS); ValidateUtils.checkTrue(this.maxFileHandleRounds >= MIN_FILE_HANDLE_ROUNDS, "File handle rounds too small: %d", this.maxFileHandleRounds); ValidateUtils.checkTrue(this.maxFileHandleRounds <= MAX_FILE_HANDLE_ROUNDS, "File handle rounds too big: %d", this.maxFileHandleRounds); if (workBuf.length < this.fileHandleSize) { workBuf = new byte[this.fileHandleSize]; } } @Override public ServerSession getServerSession() { return serverSession; } @Override public void setFileSystem(FileSystem fileSystem) { if (fileSystem != this.fileSystem) { this.fileSystem = fileSystem; Iterable<Path> roots = Objects.requireNonNull(fileSystem.getRootDirectories(), "No root directories"); Iterator<Path> available = Objects.requireNonNull(roots.iterator(), "No roots iterator"); ValidateUtils.checkTrue(available.hasNext(), "No available root"); this.defaultDir = available.next(); } } @Override public void setExitCallback(ExitCallback callback) { this.callback = callback; } @Override public void setInputStream(InputStream in) { this.in = in; } @Override public void setOutputStream(OutputStream out) { this.out = out; } @Override public void setErrorStream(OutputStream err) { this.err = err; } @Override public void start(Environment env) throws IOException { this.env = env; try { pendingFuture = executors.submit(this); } catch (RuntimeException e) { // e.g., RejectedExecutionException log.error("Failed (" + e.getClass().getSimpleName() + ") to start command: " + e.toString(), e); throw new IOException(e); } } @Override public void run() { try { for (long count = 1L;; count++) { int length = BufferUtils.readInt(in, workBuf, 0, workBuf.length); ValidateUtils.checkTrue(length >= (Integer.BYTES + 1 /* command */), "Bad length to read: %d", length); Buffer buffer = new ByteArrayBuffer(length + Integer.BYTES + Long.SIZE /* a bit extra */, false); buffer.putInt(length); for (int remainLen = length; remainLen > 0;) { int l = in.read(buffer.array(), buffer.wpos(), remainLen); if (l < 0) { throw new IllegalArgumentException("Premature EOF at buffer #" + count + " while read length=" + length + " and remain=" + remainLen); } buffer.wpos(buffer.wpos() + l); remainLen -= l; } process(buffer); } } catch (Throwable t) { if ((!closed.get()) && (!(t instanceof EOFException))) { // Ignore log.error("run({}) {} caught in SFTP subsystem: {}", getServerSession(), t.getClass().getSimpleName(), t.getMessage()); if (log.isDebugEnabled()) { log.debug("run(" + getServerSession() + ") caught exception details", t); } } } finally { handles.forEach((id, handle) -> { try { handle.close(); if (log.isDebugEnabled()) { log.debug("run({}) closed pending handle {} [{}]", getServerSession(), id, handle); } } catch (IOException ioe) { log.error("run({}) failed ({}) to close handle={}[{}]: {}", getServerSession(), ioe.getClass().getSimpleName(), id, handle, ioe.getMessage()); } }); callback.onExit(0); } } protected void process(Buffer buffer) throws IOException { int length = buffer.getInt(); int type = buffer.getUByte(); int id = buffer.getInt(); if (log.isDebugEnabled()) { log.debug("process({})[length={}, type={}, id={}] processing", getServerSession(), length, SftpConstants.getCommandMessageName(type), id); } switch (type) { case SftpConstants.SSH_FXP_INIT: doInit(buffer, id); break; case SftpConstants.SSH_FXP_OPEN: doOpen(buffer, id); break; case SftpConstants.SSH_FXP_CLOSE: doClose(buffer, id); break; case SftpConstants.SSH_FXP_READ: doRead(buffer, id); break; case SftpConstants.SSH_FXP_WRITE: doWrite(buffer, id); break; case SftpConstants.SSH_FXP_LSTAT: doLStat(buffer, id); break; case SftpConstants.SSH_FXP_FSTAT: doFStat(buffer, id); break; case SftpConstants.SSH_FXP_SETSTAT: doSetStat(buffer, id); break; case SftpConstants.SSH_FXP_FSETSTAT: doFSetStat(buffer, id); break; case SftpConstants.SSH_FXP_OPENDIR: doOpenDir(buffer, id); break; case SftpConstants.SSH_FXP_READDIR: doReadDir(buffer, id); break; case SftpConstants.SSH_FXP_REMOVE: doRemove(buffer, id); break; case SftpConstants.SSH_FXP_MKDIR: doMakeDirectory(buffer, id); break; case SftpConstants.SSH_FXP_RMDIR: doRemoveDirectory(buffer, id); break; case SftpConstants.SSH_FXP_REALPATH: doRealPath(buffer, id); break; case SftpConstants.SSH_FXP_STAT: doStat(buffer, id); break; case SftpConstants.SSH_FXP_RENAME: doRename(buffer, id); break; case SftpConstants.SSH_FXP_READLINK: doReadLink(buffer, id); break; case SftpConstants.SSH_FXP_SYMLINK: doSymLink(buffer, id); break; case SftpConstants.SSH_FXP_LINK: doLink(buffer, id); break; case SftpConstants.SSH_FXP_BLOCK: doBlock(buffer, id); break; case SftpConstants.SSH_FXP_UNBLOCK: doUnblock(buffer, id); break; case SftpConstants.SSH_FXP_EXTENDED: doExtended(buffer, id); break; default: { String name = SftpConstants.getCommandMessageName(type); log.warn("process({})[length={}, type={}, id={}] unknown command", getServerSession(), length, name, id); sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OP_UNSUPPORTED, "Command " + name + " is unsupported or not implemented"); } } if (type != SftpConstants.SSH_FXP_INIT) { requestsCount++; } } 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 void executeExtendedCommand(Buffer buffer, int id, String extension) throws IOException { switch (extension) { case SftpConstants.EXT_TEXT_SEEK: doTextSeek(buffer, id); break; case SftpConstants.EXT_VERSION_SELECT: doVersionSelect(buffer, id); break; case SftpConstants.EXT_COPY_FILE: doCopyFile(buffer, id); break; case SftpConstants.EXT_COPY_DATA: doCopyData(buffer, id); break; case SftpConstants.EXT_MD5_HASH: case SftpConstants.EXT_MD5_HASH_HANDLE: doMD5Hash(buffer, id, extension); break; case SftpConstants.EXT_CHECK_FILE_HANDLE: case SftpConstants.EXT_CHECK_FILE_NAME: doCheckFileHash(buffer, id, extension); break; case FsyncExtensionParser.NAME: doOpenSSHFsync(buffer, id); break; case SftpConstants.EXT_SPACE_AVAILABLE: doSpaceAvailable(buffer, id); break; case HardLinkExtensionParser.NAME: doOpenSSHHardLink(buffer, id); break; default: if (log.isDebugEnabled()) { log.debug("executeExtendedCommand({}) received unsupported SSH_FXP_EXTENDED({})", getServerSession(), extension); } sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_EXTENDED(" + extension + ") is unsupported or not implemented"); break; } } // 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); 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); 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); return; } sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); } protected void doTextSeek(int id, String handle, long line) throws IOException { Handle h = handles.get(handle); if (log.isDebugEnabled()) { log.debug("doTextSeek({})[id={}] SSH_FXP_EXTENDED(text-seek) (handle={}[{}], line={})", getServerSession(), id, handle, h, line); } FileHandle fileHandle = validateHandle(handle, h, FileHandle.class); throw new UnsupportedOperationException("doTextSeek(" + fileHandle + ")"); } // 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); return; } sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); } protected void doOpenSSHFsync(int id, String handle) throws IOException { Handle h = handles.get(handle); if (log.isDebugEnabled()) { log.debug("doOpenSSHFsync({})[id={}] {}[{}]", getServerSession(), id, handle, h); } FileHandle fileHandle = validateHandle(handle, h, FileHandle.class); SftpFileSystemAccessor accessor = getFileSystemAccessor(); ServerSession session = getServerSession(); accessor.syncFileData(session, this, fileHandle.getFile(), fileHandle.getFileHandle(), fileHandle.getFileChannel()); } 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); return; } send(buffer); } protected void doCheckFileHash(int id, String targetType, String target, Collection<String> algos, long startOffset, long length, int blockSize, Buffer buffer) throws Exception { Path path; if (SftpConstants.EXT_CHECK_FILE_HANDLE.equalsIgnoreCase(targetType)) { Handle h = handles.get(target); FileHandle fileHandle = validateHandle(target, h, FileHandle.class); path = fileHandle.getFile(); /* * To quote http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.2: * * If ACE4_READ_DATA was not included when the file was opened, * the server MUST return STATUS_PERMISSION_DENIED. */ int access = fileHandle.getAccessMask(); if ((access & SftpConstants.ACE4_READ_DATA) == 0) { throw new AccessDeniedException("File not opened for read: " + path); } } else { path = resolveFile(target); /* * To quote http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.2: * * If 'check-file-name' refers to a SSH_FILEXFER_TYPE_SYMLINK, the * target should be opened. */ for (int index = 0; Files.isSymbolicLink(path) && (index < Byte.MAX_VALUE /* TODO make this configurable */); index++) { path = Files.readSymbolicLink(path); } if (Files.isSymbolicLink(path)) { throw new FileSystemLoopException(target + " yields a circular or too long chain of symlinks"); } if (Files.isDirectory(path, IoUtils.getLinkOptions(false))) { throw new NotDirectoryException(path.toString()); } } ValidateUtils.checkNotNullAndNotEmpty(algos, "No hash algorithms specified"); DigestFactory factory = null; for (String a : algos) { factory = BuiltinDigests.fromFactoryName(a); if ((factory != null) && factory.isSupported()) { break; } } ValidateUtils.checkNotNull(factory, "No matching digest factory found for %s", algos); doCheckFileHash(id, path, factory, startOffset, length, blockSize, 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); return; } buffer.clear(); buffer.putByte((byte) SftpConstants.SSH_FXP_EXTENDED_REPLY); buffer.putInt(id); buffer.putString(targetType); buffer.putBytes(hashValue); send(buffer); } protected byte[] doMD5Hash( int id, String targetType, String target, long startOffset, long length, byte[] quickCheckHash) throws Exception { if (log.isDebugEnabled()) { log.debug("doMD5Hash({})({})[{}] offset={}, length={}, quick-hash={}", getServerSession(), targetType, target, startOffset, length, BufferUtils.toHex(':', quickCheckHash)); } Path path; if (SftpConstants.EXT_MD5_HASH_HANDLE.equalsIgnoreCase(targetType)) { Handle h = handles.get(target); FileHandle fileHandle = validateHandle(target, h, FileHandle.class); path = fileHandle.getFile(); /* * To quote http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.1: * * The handle MUST be a file handle, and ACE4_READ_DATA MUST * have been included in the desired-access when the file * was opened */ int access = fileHandle.getAccessMask(); if ((access & SftpConstants.ACE4_READ_DATA) == 0) { throw new AccessDeniedException("File not opened for read: " + path); } } else { path = resolveFile(target); if (Files.isDirectory(path, IoUtils.getLinkOptions(true))) { throw new NotDirectoryException(path.toString()); } } /* * To quote http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.1: * * If both start-offset and length are zero, the entire file should be included */ long effectiveLength = length; long totalSize = Files.size(path); if ((startOffset == 0L) && (length == 0L)) { effectiveLength = totalSize; } else { long maxRead = startOffset + effectiveLength; if (maxRead > totalSize) { effectiveLength = totalSize - startOffset; } } return doMD5Hash(id, path, startOffset, effectiveLength, quickCheckHash); } 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 void doVersionSelect(Buffer buffer, int id) throws IOException { String proposed = buffer.getString(); ServerSession session = getServerSession(); /* * The 'version-select' MUST be the first request from the client to the * server; if it is not, the server MUST fail the request and close the * channel. */ if (requestsCount > 0L) { sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_FAILURE, "Version selection not the 1st request for proposal = " + proposed); session.close(true); return; } Boolean result = validateProposedVersion(buffer, id, proposed); /* * "MUST then close the channel without processing any further requests" */ if (result == null) { // response sent internally session.close(true); return; } if (result) { version = Integer.parseInt(proposed); sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); } else { sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_FAILURE, "Unsupported version " + proposed); session.close(true); } } /** * @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 #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 = LOWER_SFTP_IMPL; int hig = HIGHER_SFTP_IMPL; String available = ALL_SFTP_IMPL; // check if user wants to use a specific version ServerSession session = getServerSession(); Integer sftpVersion = session.getInteger(SFTP_VERSION); if (sftpVersion != null) { int forcedValue = sftpVersion; if ((forcedValue < LOWER_SFTP_IMPL) || (forcedValue > 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 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); return; } sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); } protected void doBlock(int id, String handle, long offset, long length, int mask) throws IOException { Handle p = handles.get(handle); if (log.isDebugEnabled()) { log.debug("doBlock({})[id={}] SSH_FXP_BLOCK (handle={}[{}], offset={}, length={}, mask=0x{})", getServerSession(), id, handle, p, offset, length, Integer.toHexString(mask)); } FileHandle fileHandle = validateHandle(handle, p, FileHandle.class); SftpEventListener listener = getSftpEventListenerProxy(); ServerSession session = getServerSession(); listener.blocking(session, handle, fileHandle, offset, length, mask); try { fileHandle.lock(offset, length, mask); } catch (IOException | RuntimeException e) { listener.blocked(session, handle, fileHandle, offset, length, mask, e); throw e; } listener.blocked(session, handle, fileHandle, offset, length, mask, null); } 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); return; } sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); } protected void doUnblock(int id, String handle, long offset, long length) throws IOException { Handle p = handles.get(handle); if (log.isDebugEnabled()) { log.debug("doUnblock({})[id={}] SSH_FXP_UNBLOCK (handle={}[{}], offset={}, length={})", getServerSession(), id, handle, p, offset, length); } FileHandle fileHandle = validateHandle(handle, p, FileHandle.class); SftpEventListener listener = getSftpEventListenerProxy(); ServerSession session = getServerSession(); listener.unblocking(session, handle, fileHandle, offset, length); try { fileHandle.unlock(offset, length); } catch (IOException | RuntimeException e) { listener.unblocked(session, handle, fileHandle, offset, length, e); throw e; } listener.unblocked(session, handle, fileHandle, offset, length, null); } 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); 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); 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 void createLink(int id, String existingPath, String linkPath, boolean symLink) throws IOException { Path link = resolveFile(linkPath); Path existing = fileSystem.getPath(existingPath); if (log.isDebugEnabled()) { log.debug("createLink({})[id={}], existing={}[{}], link={}[{}], symlink={})", getServerSession(), id, linkPath, link, existingPath, existing, symLink); } SftpEventListener listener = getSftpEventListenerProxy(); ServerSession session = getServerSession(); listener.linking(session, link, existing, symLink); try { if (symLink) { Files.createSymbolicLink(link, existing); } else { Files.createLink(link, existing); } } catch (IOException | RuntimeException e) { listener.linked(session, link, existing, symLink, e); throw e; } listener.linked(session, link, existing, symLink, null); } 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); 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; if (version >= SftpConstants.SFTP_V5) { flags = buffer.getInt(); } try { doRename(id, oldPath, newPath, flags); } catch (IOException | RuntimeException e) { sendStatus(BufferUtils.clear(buffer), id, e); 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); return; } sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); } @SuppressWarnings("resource") protected void doCopyData(int id, String readHandle, long readOffset, long readLength, String writeHandle, long writeOffset) throws IOException { boolean inPlaceCopy = readHandle.equals(writeHandle); Handle rh = handles.get(readHandle); Handle wh = inPlaceCopy ? rh : handles.get(writeHandle); if (log.isDebugEnabled()) { log.debug("doCopyData({})[id={}] SSH_FXP_EXTENDED[{}] read={}[{}], read-offset={}, read-length={}, write={}[{}], write-offset={})", getServerSession(), id, SftpConstants.EXT_COPY_DATA, readHandle, rh, readOffset, readLength, writeHandle, wh, writeOffset); } FileHandle srcHandle = validateHandle(readHandle, rh, FileHandle.class); Path srcPath = srcHandle.getFile(); int srcAccess = srcHandle.getAccessMask(); if ((srcAccess & SftpConstants.ACE4_READ_DATA) != SftpConstants.ACE4_READ_DATA) { throw new AccessDeniedException("File not opened for read: " + srcPath); } ValidateUtils.checkTrue(readLength >= 0L, "Invalid read length: %d", readLength); ValidateUtils.checkTrue(readOffset >= 0L, "Invalid read offset: %d", readOffset); long totalSize = Files.size(srcHandle.getFile()); long effectiveLength = readLength; if (effectiveLength == 0L) { effectiveLength = totalSize - readOffset; } else { long maxRead = readOffset + effectiveLength; if (maxRead > totalSize) { effectiveLength = totalSize - readOffset; } } ValidateUtils.checkTrue(effectiveLength > 0L, "Non-positive effective copy data length: %d", effectiveLength); FileHandle dstHandle = inPlaceCopy ? srcHandle : validateHandle(writeHandle, wh, FileHandle.class); int dstAccess = dstHandle.getAccessMask(); if ((dstAccess & SftpConstants.ACE4_WRITE_DATA) != SftpConstants.ACE4_WRITE_DATA) { throw new AccessDeniedException("File not opened for write: " + srcHandle); } ValidateUtils.checkTrue(writeOffset >= 0L, "Invalid write offset: %d", writeOffset); // check if overlapping ranges as per the draft if (inPlaceCopy) { long maxRead = readOffset + effectiveLength; if (maxRead > totalSize) { maxRead = totalSize; } long maxWrite = writeOffset + effectiveLength; if (maxWrite > readOffset) { throw new IllegalArgumentException("Write range end [" + writeOffset + "-" + maxWrite + "]" + " overlaps with read range [" + readOffset + "-" + maxRead + "]"); } else if (maxRead > writeOffset) { throw new IllegalArgumentException("Read range end [" + readOffset + "-" + maxRead + "]" + " overlaps with write range [" + writeOffset + "-" + maxWrite + "]"); } } byte[] copyBuf = new byte[Math.min(IoUtils.DEFAULT_COPY_SIZE, (int) effectiveLength)]; while (effectiveLength > 0L) { int remainLength = Math.min(copyBuf.length, (int) effectiveLength); int readLen = srcHandle.read(copyBuf, 0, remainLength, readOffset); if (readLen < 0) { throw new EOFException("Premature EOF while still remaining " + effectiveLength + " bytes"); } dstHandle.write(copyBuf, 0, readLen, writeOffset); effectiveLength -= readLen; readOffset += readLen; writeOffset += readLen; } } // 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); 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 doStat(Buffer buffer, int id) throws IOException { String path = buffer.getString(); int flags = SftpConstants.SSH_FILEXFER_ATTR_ALL; 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); 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 { 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 FileNotFoundException(p.toString()); } 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); 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); 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, Object> attrs = readAttrs(buffer); try { doMakeDirectory(id, path, attrs, IoUtils.getLinkOptions(false)); } catch (IOException | RuntimeException e) { sendStatus(BufferUtils.clear(buffer), id, e); 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("Cannot validate make-directory existence for " + p); } if (status) { if (Files.isDirectory(p, options)) { throw new FileAlreadyExistsException(p.toString(), p.toString(), "Target directory already exists"); } else { throw new FileNotFoundException(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); 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("Cannot determine existence of remove candidate: " + p); } if (!status) { throw new FileNotFoundException(p.toString()); } 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 doReadDir(Buffer buffer, int id) throws IOException { String handle = buffer.getString(); Handle h = handles.get(handle); if (log.isDebugEnabled()) { log.debug("doReadDir({})[id={}] SSH_FXP_READDIR (handle={}[{}])", getServerSession(), id, handle, h); } Buffer reply = null; try { DirectoryHandle dh = validateHandle(handle, h, DirectoryHandle.class); if (dh.isDone()) { sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_EOF, "Directory reading is done"); return; } Path file = dh.getFile(); LinkOption[] options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_READDIR, "", file); Boolean status = IoUtils.checkFileExists(file, options); if (status == null) { throw new AccessDeniedException("Cannot determine existence of read-dir for " + file); } if (!status) { throw new FileNotFoundException(file.toString()); } else if (!Files.isDirectory(file, options)) { throw new NotDirectoryException(file.toString()); } else if (!Files.isReadable(file)) { throw new AccessDeniedException("Not readable: " + file.toString()); } if (dh.isSendDot() || dh.isSendDotDot() || dh.hasNext()) { // There is at least one file in the directory or we need to send the "..". // Send only a few files at a time to not create packets of a too // large size or have a timeout to occur. reply = BufferUtils.clear(buffer); reply.putByte((byte) SftpConstants.SSH_FXP_NAME); reply.putInt(id); int lenPos = reply.wpos(); reply.putInt(0); ServerSession session = getServerSession(); int maxDataSize = session.getIntProperty(MAX_READDIR_DATA_SIZE_PROP, DEFAULT_MAX_READDIR_DATA_SIZE); int count = doReadDir(id, handle, dh, reply, maxDataSize, IoUtils.getLinkOptions(false)); BufferUtils.updateLengthPlaceholder(reply, lenPos, count); if ((!dh.isSendDot()) && (!dh.isSendDotDot()) && (!dh.hasNext())) { dh.markDone(); } Boolean indicator = SftpHelper.indicateEndOfNamesList(reply, getVersion(), session, dh.isDone()); if (log.isDebugEnabled()) { log.debug("doReadDir({})({})[{}] - seding {} entries - eol={}", session, handle, h, count, indicator); } } else { // empty directory dh.markDone(); sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_EOF, "Empty directory"); return; } Objects.requireNonNull(reply, "No reply buffer created"); } catch (IOException | RuntimeException e) { sendStatus(BufferUtils.clear(buffer), id, e); return; } send(reply); } 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); return; } sendHandle(BufferUtils.clear(buffer), id, handle); } protected String doOpenDir(int id, String path, Path p, LinkOption... options) throws IOException { Boolean status = IoUtils.checkFileExists(p, options); if (status == null) { throw new AccessDeniedException("Cannot determine open-dir existence for " + p); } if (!status) { throw new FileNotFoundException(path); } else if (!Files.isDirectory(p, options)) { throw new NotDirectoryException(path); } else if (!Files.isReadable(p)) { throw new AccessDeniedException("Not readable: " + p); } else { String handle = generateFileHandle(p); DirectoryHandle dirHandle = new DirectoryHandle(this, p, handle); handles.put(handle, dirHandle); return handle; } } 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); return; } sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); } protected void doFSetStat(int id, String handle, Map<String, ?> attrs) throws IOException { Handle h = handles.get(handle); if (log.isDebugEnabled()) { log.debug("doFsetStat({})[id={}] SSH_FXP_FSETSTAT (handle={}[{}], attrs={})", getServerSession(), id, handle, h, attrs); } doSetAttributes(validateHandle(handle, h, Handle.class).getFile(), attrs); } 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); 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; 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); return; } sendAttrs(BufferUtils.clear(buffer), id, attrs); } protected Map<String, Object> doFStat(int id, String handle, int flags) throws IOException { Handle h = handles.get(handle); if (log.isDebugEnabled()) { log.debug("doFStat({})[id={}] SSH_FXP_FSTAT (handle={}[{}], flags=0x{})", getServerSession(), id, handle, h, Integer.toHexString(flags)); } return resolveFileAttributes(validateHandle(handle, h, Handle.class).getFile(), flags, IoUtils.getLinkOptions(true)); } protected void doLStat(Buffer buffer, int id) throws IOException { String path = buffer.getString(); int flags = SftpConstants.SSH_FILEXFER_ATTR_ALL; 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); 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 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); return; } sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, ""); } protected void doWrite(int id, String handle, long offset, int length, byte[] data, int doff, int remaining) throws IOException { Handle h = handles.get(handle); if (log.isTraceEnabled()) { log.trace("doWrite({})[id={}] SSH_FXP_WRITE (handle={}[{}], offset={}, data=byte[{}])", getServerSession(), id, handle, h, offset, length); } FileHandle fh = validateHandle(handle, h, FileHandle.class); if (length < 0) { throw new IllegalStateException("Bad length (" + length + ") for writing to " + fh); } if (remaining < length) { throw new IllegalStateException("Not enough buffer data for writing to " + fh + ": required=" + length + ", available=" + remaining); } SftpEventListener listener = getSftpEventListenerProxy(); listener.writing(getServerSession(), handle, fh, offset, data, doff, length); try { if (fh.isOpenAppend()) { fh.append(data, doff, length); } else { fh.write(data, doff, length, offset); } } catch (IOException | RuntimeException e) { listener.written(getServerSession(), handle, fh, offset, data, doff, length, e); throw e; } listener.written(getServerSession(), handle, fh, offset, data, doff, length, null); } protected void doRead(Buffer buffer, int id) throws IOException { String handle = buffer.getString(); long offset = buffer.getLong(); int requestedLength = buffer.getInt(); int maxAllowed = getServerSession().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={}", getServerSession(), 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); return; } send(buffer); } protected int doRead(int id, String handle, long offset, int length, byte[] data, int doff) throws IOException { Handle h = handles.get(handle); if (log.isTraceEnabled()) { log.trace("doRead({})[id={}] SSH_FXP_READ (handle={}[{}], offset={}, length={})", getServerSession(), id, handle, h, offset, length); } ValidateUtils.checkTrue(length > 0L, "Invalid read length: %d", length); FileHandle fh = validateHandle(handle, h, FileHandle.class); SftpEventListener listener = getSftpEventListenerProxy(); ServerSession serverSession = getServerSession(); int readLen; listener.reading(serverSession, handle, fh, offset, data, doff, length); try { readLen = fh.read(data, doff, length, offset); } catch (IOException | RuntimeException e) { listener.read(serverSession, handle, fh, offset, data, doff, length, -1, e); throw e; } listener.read(serverSession, handle, fh, offset, data, doff, length, readLen, null); return readLen; } 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); return; } sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "", ""); } protected void doClose(int id, String handle) throws IOException { Handle h = handles.remove(handle); if (log.isDebugEnabled()) { log.debug("doClose({})[id={}] SSH_FXP_CLOSE (handle={}[{}])", getServerSession(), id, handle, h); } validateHandle(handle, h, Handle.class).close(); SftpEventListener listener = getSftpEventListenerProxy(); listener.close(getServerSession(), handle, h); } 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; 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); 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 String doOpen(int id, String path, int pflags, int access, Map<String, Object> attrs) throws IOException { if (log.isDebugEnabled()) { log.debug("doOpen({})[id={}] SSH_FXP_OPEN (path={}, access=0x{}, pflags=0x{}, attrs={})", getServerSession(), id, path, Integer.toHexString(access), Integer.toHexString(pflags), attrs); } int curHandleCount = handles.size(); int maxHandleCount = getServerSession().getIntProperty(MAX_OPEN_HANDLES_PER_SESSION, DEFAULT_MAX_OPEN_HANDLES); if (curHandleCount > maxHandleCount) { throw new IllegalStateException("Too many open handles: current=" + curHandleCount + ", max.=" + maxHandleCount); } Path file = resolveFile(path); String handle = generateFileHandle(file); FileHandle fileHandle = new FileHandle(this, file, handle, pflags, access, attrs); handles.put(handle, fileHandle); return handle; } // we stringify our handles and treat them as such on decoding as well as it is easier to use as a map key protected String generateFileHandle(Path file) { // use several rounds in case the file handle size is relatively small so we might get conflicts for (int index = 0; index < maxFileHandleRounds; index++) { randomizer.fill(workBuf, 0, fileHandleSize); String handle = BufferUtils.toHex(workBuf, 0, fileHandleSize, BufferUtils.EMPTY_HEX_SEPARATOR); if (handles.containsKey(handle)) { if (log.isTraceEnabled()) { log.trace("generateFileHandle({})[{}] handle={} in use at round {}", getServerSession(), file, handle, index); } continue; } if (log.isTraceEnabled()) { log.trace("generateFileHandle({})[{}] {}", getServerSession(), file, handle); } return handle; } throw new IllegalStateException("Failed to generate a unique file handle for " + file); } protected void doInit(Buffer buffer, int id) throws IOException { if (log.isDebugEnabled()) { log.debug("doInit({})[id={}] SSH_FXP_INIT (version={})", getServerSession(), id, id); } String all = checkVersionCompatibility(buffer, id, id, SftpConstants.SSH_FX_OP_UNSUPPORTED); if (GenericUtils.isEmpty(all)) { // i.e. validation failed return; } version = id; while (buffer.available() > 0) { String name = buffer.getString(); byte[] data = buffer.getBytes(); extensions.put(name, data); } buffer.clear(); buffer.putByte((byte) SftpConstants.SSH_FXP_VERSION); buffer.putInt(version); appendExtensions(buffer, all); SftpEventListener listener = getSftpEventListenerProxy(); listener.initialized(getServerSession(), version); send(buffer); } 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(); 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); 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); 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 Map<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 FileNotFoundException(file.toString()); } else { return getAttributes(file, flags, options); } } protected void writeAttrs(Buffer buffer, Map<String, ?> attributes) throws IOException { SftpHelper.writeAttrs(buffer, getVersion(), attributes); } protected Map<String, Object> getAttributes(Path file, LinkOption... options) throws IOException { return getAttributes(file, SftpConstants.SSH_FILEXFER_ATTR_ALL, options); } protected Map<String, Object> handleUnknownStatusFileAttributes(Path file, int flags, LinkOption... options) throws IOException { switch (unsupportedAttributePolicy) { case Ignore: break; case ThrowException: throw new AccessDeniedException("Cannot determine existence for attributes of " + file); 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 Map<String, Object> getAttributes(Path file, int flags, LinkOption... options) throws IOException { FileSystem fs = file.getFileSystem(); Collection<String> supportedViews = fs.supportedFileAttributeViews(); Map<String, Object> attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); Collection<String> views; if (GenericUtils.isEmpty(supportedViews)) { views = Collections.emptyList(); } else if (supportedViews.contains("unix")) { views = DEFAULT_UNIX_VIEW; } else { views = GenericUtils.map(supportedViews, v -> v + ":*"); } for (String v : views) { Map<String, Object> ta = readFileAttributes(file, v, options); if (GenericUtils.isNotEmpty(ta)) { attrs.putAll(ta); } } Map<String, Object> completions = resolveMissingFileAttributes(file, flags, attrs, options); if (GenericUtils.isNotEmpty(completions)) { attrs.putAll(completions); } return attrs; } /** * Called by {@link #getAttributes(Path, int, LinkOption...)} in order * to complete any attributes that could not be retrieved via the supported * file system views. These attributes are deemed important so an extra * effort is made to provide a value for them * @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 current The {@link Map} of attributes already retrieved - may be * {@code null}/empty and/or unmodifiable * @param options The {@link LinkOption}s to use in order to access the file * if necessary * @return A {@link Map} of the extra attributes whose values need to be * updated in the original map. <B>Note:</B> it is allowed to specify values * which <U>override</U> existing ones - the default implementation does not * override values that have a non-{@code null} value * @throws IOException If failed to access the attributes - in which case * an <U>error</U> is returned to the SFTP client * @see #FILEATTRS_RESOLVERS */ protected Map<String, Object> resolveMissingFileAttributes(Path file, int flags, Map<String, Object> current, LinkOption... options) throws IOException { Map<String, Object> attrs = null; // Cannot use forEach because the attrs variable is not effectively final for (Map.Entry<String, FileInfoExtractor<?>> re : FILEATTRS_RESOLVERS.entrySet()) { String name = re.getKey(); Object value = GenericUtils.isEmpty(current) ? null : current.get(name); FileInfoExtractor<?> x = re.getValue(); try { Object resolved = resolveMissingFileAttributeValue(file, name, value, x, options); if (Objects.equals(resolved, value)) { continue; } if (attrs == null) { attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } attrs.put(name, resolved); if (log.isDebugEnabled()) { log.debug("resolveMissingFileAttributes({})[{}[{}]] replace {} with {}", getServerSession(), file, name, value, resolved); } } catch (IOException e) { if (log.isDebugEnabled()) { log.debug("resolveMissingFileAttributes({})[{}[{}]] failed ({}) to resolve missing value: {}", getServerSession(), file, name, e.getClass().getSimpleName(), e.getMessage()); } if (log.isTraceEnabled()) { log.trace("resolveMissingFileAttributes(" + getServerSession() + ")" + "[" + file + "[" + name + "]] missing value resolution failure details", e); } } } if (attrs == null) { return Collections.emptyMap(); } else { return attrs; } } protected Object resolveMissingFileAttributeValue(Path file, String name, Object value, FileInfoExtractor<?> x, LinkOption... options) throws IOException { if (value != null) { return value; } else { return x.infoOf(file, options); } } protected Map<String, Object> addMissingAttribute(Path file, Map<String, Object> current, String name, FileInfoExtractor<?> x, LinkOption... options) throws IOException { Object value = GenericUtils.isEmpty(current) ? null : current.get(name); if (value != null) { // already have the value return current; } // skip if still no value value = x.infoOf(file, options); if (value == null) { return current; } if (current == null) { current = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } current.put(name, value); return current; } protected Map<String, Object> readFileAttributes(Path file, String view, LinkOption... options) throws IOException { try { return Files.readAttributes(file, view, options); } catch (IOException e) { return handleReadFileAttributesException(file, view, options, e); } } protected Map<String, Object> handleReadFileAttributesException(Path file, String view, LinkOption[] options, IOException e) throws IOException { if (log.isTraceEnabled()) { log.trace("handleReadFileAttributesException(" + file + ")[" + view + "] details", e); } switch (unsupportedAttributePolicy) { case Ignore: break; case Warn: log.warn("handleReadFileAttributesException(" + file + ")[" + view + "] " + e.getClass().getSimpleName() + ": " + e.getMessage()); break; case ThrowException: throw e; default: log.warn("handleReadFileAttributesException(" + file + ")[" + view + "]" + " Unknown policy (" + unsupportedAttributePolicy + ")" + " for " + e.getClass().getSimpleName() + ": " + e.getMessage()); } return Collections.emptyMap(); } protected void doSetAttributes(Path file, Map<String, ?> attributes) throws IOException { SftpEventListener listener = getSftpEventListenerProxy(); ServerSession session = getServerSession(); listener.modifyingAttributes(session, file, attributes); try { setFileAttributes(file, attributes, IoUtils.getLinkOptions(false)); } catch (IOException | RuntimeException e) { listener.modifiedAttributes(session, file, attributes, e); throw e; } listener.modifiedAttributes(session, file, attributes, null); } protected LinkOption[] getPathResolutionLinkOption(int cmd, String extension, Path path) throws IOException { ServerSession session = getServerSession(); boolean followLinks = PropertyResolverUtils.getBooleanProperty(session, AUTO_FOLLOW_LINKS, DEFAULT_AUTO_FOLLOW_LINKS); return IoUtils.getLinkOptions(followLinks); } protected void setFileAttributes(Path file, Map<String, ?> attributes, LinkOption... options) throws IOException { Set<String> unsupported = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); // Cannot use forEach because of the potential IOException being thrown for (Map.Entry<String, ?> ae : attributes.entrySet()) { String attribute = ae.getKey(); Object value = ae.getValue(); String view = null; switch (attribute) { case "size": { long newSize = ((Number) value).longValue(); SftpFileSystemAccessor accessor = getFileSystemAccessor(); try (SeekableByteChannel channel = accessor.openFile(getServerSession(), this, file, null, EnumSet.of(StandardOpenOption.WRITE))) { channel.truncate(newSize); } continue; } case "uid": view = "unix"; break; case "gid": view = "unix"; break; case "owner": view = "posix"; value = toUser(file, (UserPrincipal) value); break; case "group": view = "posix"; value = toGroup(file, (GroupPrincipal) value); break; case "permissions": view = "posix"; break; case "acl": view = "acl"; break; case "creationTime": view = "basic"; break; case "lastModifiedTime": view = "basic"; break; case "lastAccessTime": view = "basic"; break; case "extended": view = "extended"; break; default: // ignored } if ((GenericUtils.length(view) > 0) && (value != null)) { try { setFileAttribute(file, view, attribute, value, options); } catch (Exception e) { handleSetFileAttributeFailure(file, view, attribute, value, unsupported, e); } } } handleUnsupportedAttributes(unsupported); } protected void handleSetFileAttributeFailure(Path file, String view, String attribute, Object value, Collection<String> unsupported, Exception e) throws IOException { if (e instanceof UnsupportedOperationException) { if (log.isDebugEnabled()) { log.debug("handleSetFileAttributeFailure({})[{}] {}:{}={} unsupported: {}", getServerSession(), file, view, attribute, value, e.getMessage()); } unsupported.add(attribute); } else { log.warn("handleSetFileAttributeFailure({})[{}] {}:{}={} - failed ({}) to set: {}", getServerSession(), file, view, attribute, value, e.getClass().getSimpleName(), e.getMessage()); if (log.isDebugEnabled()) { log.debug("handleSetFileAttributeFailure(" + getServerSession() + ")" + "[" + file + "] " + view + ":" + attribute + "=" + value + " failure details", e); } if (e instanceof IOException) { throw (IOException) e; } else { throw new IOException(e); } } } protected void setFileAttribute(Path file, String view, String attribute, Object value, LinkOption... options) throws IOException { if (log.isTraceEnabled()) { log.trace("setFileAttribute({})[{}] {}:{}={}", getServerSession(), file, view, attribute, value); } if ("acl".equalsIgnoreCase(attribute) && "acl".equalsIgnoreCase(view)) { @SuppressWarnings("unchecked") List<AclEntry> acl = (List<AclEntry>) value; setFileAccessControl(file, acl, options); } else if ("permissions".equalsIgnoreCase(attribute)) { @SuppressWarnings("unchecked") Set<PosixFilePermission> perms = (Set<PosixFilePermission>) value; setFilePermissions(file, perms, options); } else if ("owner".equalsIgnoreCase(attribute) || "group".equalsIgnoreCase(attribute)) { setFileOwnership(file, attribute, (Principal) value, options); } else if ("creationTime".equalsIgnoreCase(attribute) || "lastModifiedTime".equalsIgnoreCase(attribute) || "lastAccessTime".equalsIgnoreCase(attribute)) { setFileTime(file, view, attribute, (FileTime) value, options); } else if ("extended".equalsIgnoreCase(view) && "extended".equalsIgnoreCase(attribute)) { @SuppressWarnings("unchecked") Map<String, byte[]> extensions = (Map<String, byte[]>) value; setFileExtensions(file, extensions, options); } else { Files.setAttribute(file, view + ":" + attribute, value, options); } } protected void setFileTime(Path file, String view, String attribute, FileTime value, LinkOption... options) throws IOException { if (value == null) { return; } if (log.isDebugEnabled()) { log.debug("setFileTime({})[{}] {}:{}={}", getServerSession(), file, view, attribute, value); } Files.setAttribute(file, view + ":" + attribute, value, options); } protected void setFileOwnership(Path file, String attribute, Principal value, LinkOption... options) throws IOException { if (value == null) { return; } if (log.isDebugEnabled()) { log.debug("setFileOwnership({})[{}] {}={}", getServerSession(), file, attribute, value); } /* * Quoting from Javadoc of FileOwnerAttributeView#setOwner: * * To ensure consistent and correct behavior across platforms * it is recommended that this method should only be used * to set the file owner to a user principal that is not a group. */ if ("owner".equalsIgnoreCase(attribute)) { FileOwnerAttributeView view = Files.getFileAttributeView(file, FileOwnerAttributeView.class, options); if (view == null) { throw new UnsupportedOperationException("Owner view not supported for " + file); } if (!(value instanceof UserPrincipal)) { throw new StreamCorruptedException("Owner is not " + UserPrincipal.class.getSimpleName() + ": " + value.getClass().getSimpleName()); } view.setOwner((UserPrincipal) value); } else if ("group".equalsIgnoreCase(attribute)) { PosixFileAttributeView view = Files.getFileAttributeView(file, PosixFileAttributeView.class, options); if (view == null) { throw new UnsupportedOperationException("POSIX view not supported"); } if (!(value instanceof GroupPrincipal)) { throw new StreamCorruptedException("Group is not " + GroupPrincipal.class.getSimpleName() + ": " + value.getClass().getSimpleName()); } view.setGroup((GroupPrincipal) value); } else { throw new UnsupportedOperationException("Unknown ownership attribute: " + attribute); } } protected void setFileExtensions(Path file, Map<String, byte[]> extensions, LinkOption... options) throws IOException { if (GenericUtils.isEmpty(extensions)) { return; } /* According to v3,4,5: * * Implementations SHOULD ignore extended data fields that they do not understand. * * But according to v6 (https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-28): * Implementations MUST return SSH_FX_UNSUPPORTED if there are any unrecognized extensions. */ if (version < SftpConstants.SFTP_V6) { if (log.isDebugEnabled()) { log.debug("setFileExtensions({})[{}]: {}", getServerSession(), file, extensions); } } else { throw new UnsupportedOperationException("File extensions not supported"); } } protected void setFilePermissions(Path file, Set<PosixFilePermission> perms, LinkOption... options) throws IOException { if (OsUtils.isWin32()) { IoUtils.setPermissionsToFile(file.toFile(), perms); return; } PosixFileAttributeView view = Files.getFileAttributeView(file, PosixFileAttributeView.class, options); if (view == null) { throw new UnsupportedOperationException("POSIX view not supported for " + file); } if (log.isTraceEnabled()) { log.trace("setFilePermissions({})[{}] {}", getServerSession(), file, perms); } view.setPermissions(perms); } protected void setFileAccessControl(Path file, List<AclEntry> acl, LinkOption... options) throws IOException { AclFileAttributeView view = Files.getFileAttributeView(file, AclFileAttributeView.class, options); if (view == null) { throw new UnsupportedOperationException("ACL view not supported for " + file); } if (log.isTraceEnabled()) { log.trace("setFileAccessControl({})[{}] {}", getServerSession(), file, acl); } view.setAcl(acl); } protected void handleUnsupportedAttributes(Collection<String> attributes) { if (attributes.isEmpty()) { return; } String attrsList = GenericUtils.join(attributes, ','); switch (unsupportedAttributePolicy) { case Ignore: break; case Warn: log.warn("Unsupported attributes: " + attrsList); break; case ThrowException: throw new UnsupportedOperationException("Unsupported attributes: " + attrsList); default: log.warn("Unknown policy for attributes=" + attrsList + ": " + unsupportedAttributePolicy); } } protected GroupPrincipal toGroup(Path file, GroupPrincipal name) throws IOException { String groupName = name.toString(); FileSystem fileSystem = file.getFileSystem(); UserPrincipalLookupService lookupService = fileSystem.getUserPrincipalLookupService(); try { if (lookupService == null) { throw new UserPrincipalNotFoundException(groupName); } return lookupService.lookupPrincipalByGroupName(groupName); } catch (IOException e) { handleUserPrincipalLookupServiceException(GroupPrincipal.class, groupName, e); return null; } } protected UserPrincipal toUser(Path file, UserPrincipal name) throws IOException { String username = name.toString(); FileSystem fileSystem = file.getFileSystem(); UserPrincipalLookupService lookupService = fileSystem.getUserPrincipalLookupService(); try { if (lookupService == null) { throw new UserPrincipalNotFoundException(username); } return lookupService.lookupPrincipalByName(username); } catch (IOException e) { handleUserPrincipalLookupServiceException(UserPrincipal.class, username, e); return null; } } protected void handleUserPrincipalLookupServiceException(Class<? extends Principal> principalType, String name, IOException e) throws IOException { if (log.isTraceEnabled()) { log.trace("handleUserPrincipalLookupServiceException(" + principalType.getSimpleName() + "[" + name + "]) details", e); } /* According to Javadoc: * * "Where an implementation does not support any notion of group * or user then this method always throws UserPrincipalNotFoundException." */ switch (unsupportedAttributePolicy) { case Ignore: break; case Warn: log.warn("handleUserPrincipalLookupServiceException(" + principalType.getSimpleName() + "[" + name + "])" + " failed (" + e.getClass().getSimpleName() + "): " + e.getMessage()); break; case ThrowException: throw e; default: log.warn("Unknown policy for principal=" + principalType.getSimpleName() + "[" + name + "]: " + unsupportedAttributePolicy); } } protected Map<String, Object> readAttrs(Buffer buffer) throws IOException { return SftpHelper.readAttrs(buffer, getVersion()); } /** * Makes sure that the local handle is not null and of the specified type * * @param <H> The generic handle type * @param handle The original handle id * @param h The resolved {@link Handle} instance * @param type The expected handle type * @return The cast type * @throws IOException If a generic exception occurred * @throws FileNotFoundException If the handle instance is {@code null} * @throws InvalidHandleException If the handle instance is not of the expected type */ protected <H extends Handle> H validateHandle(String handle, Handle h, Class<H> type) throws IOException { if (h == null) { throw new FileNotFoundException("No such current handle: " + handle); } Class<?> t = h.getClass(); if (!type.isAssignableFrom(t)) { throw new InvalidHandleException(handle, h, type); } return type.cast(h); } protected void sendStatus(Buffer buffer, int id, Throwable e) throws IOException { int substatus = SftpHelper.resolveSubstatus(e); sendStatus(buffer, id, substatus, SftpHelper.resolveStatusMessage(e)); } protected void sendStatus(Buffer buffer, int id, int substatus, String msg) throws IOException { sendStatus(buffer, id, substatus, (msg != null) ? msg : "", ""); } protected void sendStatus(Buffer buffer, int id, int substatus, String msg, String lang) throws IOException { if (log.isDebugEnabled()) { log.debug("doSendStatus({})[id={}] SSH_FXP_STATUS (substatus={}, lang={}, msg={})", getServerSession(), id, SftpConstants.getStatusName(substatus), lang, msg); } buffer.putByte((byte) SftpConstants.SSH_FXP_STATUS); buffer.putInt(id); buffer.putInt(substatus); buffer.putString(msg); buffer.putString(lang); send(buffer); } protected void send(Buffer buffer) throws IOException { int len = buffer.available(); BufferUtils.writeInt(out, len, workBuf, 0, workBuf.length); out.write(buffer.array(), buffer.rpos(), len); out.flush(); } @Override public void destroy() { if (closed.getAndSet(true)) { return; // ignore if already closed } ServerSession session = getServerSession(); if (log.isDebugEnabled()) { log.debug("destroy({}) - mark as closed", session); } try { SftpEventListener listener = getSftpEventListenerProxy(); listener.destroying(session); } catch (Exception e) { log.warn("destroy({}) Failed ({}) to announce destruction event: {}", session, e.getClass().getSimpleName(), e.getMessage()); if (log.isDebugEnabled()) { log.debug("destroy(" + session + ") destruction announcement failure details", e); } } // if thread has not completed, cancel it if ((pendingFuture != null) && (!pendingFuture.isDone())) { boolean result = pendingFuture.cancel(true); // TODO consider waiting some reasonable (?) amount of time for cancellation if (log.isDebugEnabled()) { log.debug("destroy(" + session + ") - cancel pending future=" + result); } } pendingFuture = null; if ((executors != null) && (!executors.isShutdown()) && shutdownExecutor) { Collection<Runnable> runners = executors.shutdownNow(); if (log.isDebugEnabled()) { log.debug("destroy(" + session + ") - shutdown executor service - runners count=" + runners.size()); } } executors = null; try { fileSystem.close(); } catch (UnsupportedOperationException e) { if (log.isDebugEnabled()) { log.debug("destroy(" + session + ") closing the file system is not supported"); } } catch (IOException e) { if (log.isDebugEnabled()) { log.debug("destroy(" + session + ")" + " failed (" + e.getClass().getSimpleName() + ")" + " to close file system: " + e.getMessage(), e); } } } protected Path resolveNormalizedLocation(String remotePath) throws IOException, InvalidPathException { return normalize(resolveFile(remotePath)); } protected Path normalize(Path f) { if (f == null) { return null; } Path abs = f.isAbsolute() ? f : f.toAbsolutePath(); return abs.normalize(); } /** * @param remotePath The remote path - separated by '/' * @return The local {@link Path} * @throws IOException If failed to resolve the local path * @throws InvalidPathException If bad local path specification */ protected Path resolveFile(String remotePath) throws IOException, InvalidPathException { String path = SelectorUtils.translateToLocalFileSystemPath(remotePath, '/', defaultDir.getFileSystem()); Path p = defaultDir.resolve(path); if (log.isTraceEnabled()) { log.trace("resolveFile({}) {} => {}", getServerSession(), remotePath, p); } return p; } }