This is an automated email from the ASF dual-hosted git repository. lgoldstein pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
The following commit(s) were added to refs/heads/master by this push: new 95de1a6 [SSHD-992] Provide more hooks to access SFTP backend file via SftpFileSystemAccessor 95de1a6 is described below commit 95de1a6614bd451744c3822c533dd2b34022c428 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Fri May 15 05:10:15 2020 +0300 [SSHD-992] Provide more hooks to access SFTP backend file via SftpFileSystemAccessor --- CHANGES.md | 15 +- .../sftp/AbstractSftpSubsystemHelper.java | 153 ++++------- .../subsystem/sftp/SftpFileSystemAccessor.java | 289 +++++++++++++++++++-- .../sshd/server/subsystem/sftp/SftpSubsystem.java | 7 +- .../server/subsystem/sftp/SftpSubsystemProxy.java | 29 +++ .../sshd/client/subsystem/sftp/SftpTest.java | 6 +- 6 files changed, 379 insertions(+), 120 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0fbc103..76c083f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,14 @@ translated internally into same code flow as if an `SSH_MSH_REQUEST_FAILURE` has been received - see [SSHD-968](https://issues.apache.org/jira/browse/SSHD-968). +* Server SFTP subsystem internal code dealing with the local files has been delegated to +the `SftpFileSystemAccessor` in order to allow easier hooking into the SFTP subsystem. + + * Resolving a local file path for an SFTP remote one + * Reading/Writing a file's attribute(s) + * Creating files links + * Copying / Renaming / Deleting files + ## Minor code helpers * Handling of debug/ignore/unimplemented messages has been split into `handleXXX` and `doInvokeXXXMsgHandler` methods @@ -19,6 +27,9 @@ where the former validate the messages and deal with the idle timeout, and the l * Added overloaded methods that accept a `java.time.Duration` specifier for timeout value. +* The argument representing the SFTP subsystem in invocations to `SftpFileSystemAccessor` has been enhanced to expose +as much of the available functionality as possible. + ## Behavioral changes and enhancements * [SSHD-964](https://issues.apache.org/jira/browse/SSHD-964) - Send SSH_MSG_CHANNEL_EOF when tunnel channel being closed. @@ -35,4 +46,6 @@ where the former validate the messages and deal with the idle timeout, and the l * [SSHD-660](https://issues.apache.org/jira/browse/SSHD-660) - Added support for server-side signed certificate keys -* [SSHD-984](https://issues.apache.org/jira/browse/SSHD-984) - Utility method to export KeyPair in OpenSSH format \ No newline at end of file +* [SSHD-984](https://issues.apache.org/jira/browse/SSHD-984) - Utility method to export KeyPair in OpenSSH format + +* [SSHD-992](https://issues.apache.org/jira/browse/SSHD-984) - Provide more hooks into the SFTP server subsystem via SftpFileSystemAccessor \ No newline at end of file diff --git a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java index 4d2144d..ef644a3 100644 --- a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java +++ b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpSubsystemHelper.java @@ -23,7 +23,6 @@ import java.io.EOFException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.StreamCorruptedException; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; import java.nio.charset.StandardCharsets; @@ -41,15 +40,10 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.AclEntry; -import java.nio.file.attribute.AclFileAttributeView; -import java.nio.file.attribute.FileOwnerAttributeView; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.GroupPrincipal; -import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.UserPrincipal; -import java.nio.file.attribute.UserPrincipalLookupService; -import java.nio.file.attribute.UserPrincipalNotFoundException; import java.security.Principal; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.ArrayList; @@ -95,7 +89,6 @@ import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.MapEntryUtils.NavigableMapBuilder; import org.apache.sshd.common.util.NumberUtils; import org.apache.sshd.common.util.OsUtils; -import org.apache.sshd.common.util.SelectorUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.BufferUtils; @@ -110,10 +103,10 @@ import org.apache.sshd.server.session.ServerSession; @SuppressWarnings("checkstyle:MethodCount") // TODO split this big class and remove the suppression public abstract class AbstractSftpSubsystemHelper extends AbstractLoggingBean - implements SftpEventListenerManager, SftpSubsystemEnvironment { + implements SftpSubsystemProxy { /** * 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"; @@ -1181,13 +1174,14 @@ public abstract class AbstractSftpSubsystemHelper } protected String doReadLink(int id, String path) throws IOException { - Path f = resolveFile(path); - Path t = Files.readSymbolicLink(f); + Path link = resolveFile(path); + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + String target = accessor.resolveLinkTarget(getServerSession(), this, link); if (log.isDebugEnabled()) { log.debug("doReadLink({})[id={}] path={}[{}]: {}", - getServerSession(), id, path, f, t); + getServerSession(), id, path, link, target); } - return t.toString(); + return target; } protected void doRename(Buffer buffer, int id) throws IOException { @@ -1237,10 +1231,8 @@ public abstract class AbstractSftpSubsystemHelper listener.moving(session, o, n, opts); try { - Files.move(o, n, - GenericUtils.isEmpty(opts) - ? IoUtils.EMPTY_COPY_OPTIONS - : opts.toArray(new CopyOption[opts.size()])); + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + accessor.renameFile(session, this, o, n, opts); } catch (IOException | RuntimeException e) { listener.moved(session, o, n, opts, e); throw e; @@ -1308,10 +1300,8 @@ public abstract class AbstractSftpSubsystemHelper 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()])); + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + accessor.copyFile(getServerSession(), this, src, dst, opts); } protected void doBlock(Buffer buffer, int id) throws IOException { @@ -1582,7 +1572,8 @@ public abstract class AbstractSftpSubsystemHelper listener.removing(session, p, isDirectory); try { - Files.delete(p); + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + accessor.removeFile(session, this, p, isDirectory); } catch (IOException | RuntimeException e) { listener.removed(session, p, isDirectory, e); throw e; @@ -1632,7 +1623,8 @@ public abstract class AbstractSftpSubsystemHelper SftpEventListener listener = getSftpEventListenerProxy(); listener.creating(session, p, attrs); try { - Files.createDirectory(p); + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + accessor.createDirectory(session, this, p); boolean followLinks = resolvePathResolutionFollowLinks( SftpConstants.SSH_FXP_MKDIR, "", p); doSetAttributes(p, attrs, followLinks); @@ -2370,7 +2362,7 @@ public abstract class AbstractSftpSubsystemHelper * 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 @@ -2465,7 +2457,9 @@ public abstract class AbstractSftpSubsystemHelper Path file, String view, LinkOption... options) throws IOException { try { - Map<String, ?> attrs = Files.readAttributes(file, view, options); + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + Map<String, ?> attrs = accessor.readFileAttributes( + getServerSession(), this, file, view, options); if (GenericUtils.isEmpty(attrs)) { return Collections.emptyNavigableMap(); } @@ -2654,7 +2648,7 @@ public abstract class AbstractSftpSubsystemHelper Map<String, byte[]> extensions = (Map<String, byte[]>) value; setFileExtensions(file, extensions, options); } else { - Files.setAttribute(file, view + ":" + attribute, value, options); + setFileRawViewAttribute(file, view, attribute, value, options); } } @@ -2670,18 +2664,23 @@ public abstract class AbstractSftpSubsystemHelper getServerSession(), file, view, attribute, value); } - Files.setAttribute(file, view + ":" + attribute, value, options); + setFileRawViewAttribute(file, view, attribute, value, options); + } + + protected void setFileRawViewAttribute( + Path file, String view, String attribute, Object value, LinkOption... options) + throws IOException { + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + accessor.setFileAttribute( + getServerSession(), this, file, view, attribute, value, options); } protected void setFileOwnership( Path file, String attribute, Principal value, LinkOption... options) throws IOException { - if (value == null) { - return; - } - + ServerSession serverSession = getServerSession(); if (log.isDebugEnabled()) { - log.debug("setFileOwnership({})[{}] {}={}", getServerSession(), file, attribute, value); + log.debug("setFileOwnership({})[{}] {}={}", serverSession, file, attribute, value); } /* @@ -2690,30 +2689,11 @@ public abstract class AbstractSftpSubsystemHelper * 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. */ + SftpFileSystemAccessor accessor = getFileSystemAccessor(); 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); + accessor.setFileOwner(serverSession, this, file, value, options); } 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); + accessor.setGroupOwner(serverSession, this, file, value, options); } else { throw new UnsupportedOperationException("Unknown ownership attribute: " + attribute); } @@ -2747,34 +2727,26 @@ public abstract class AbstractSftpSubsystemHelper 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); - } - + ServerSession serverSession = getServerSession(); if (log.isTraceEnabled()) { - log.trace("setFilePermissions({})[{}] {}", getServerSession(), file, perms); + log.trace("setFilePermissions({})[{}] {}", serverSession, file, perms); } - view.setPermissions(perms); + + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + accessor.setFilePermissions( + serverSession, this, file, perms, options); } 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); - } - + ServerSession serverSession = getServerSession(); if (log.isTraceEnabled()) { - log.trace("setFileAccessControl({})[{}] {}", getServerSession(), file, acl); + log.trace("setFileAccessControl({})[{}] {}", serverSession, file, acl); } - view.setAcl(acl); + + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + accessor.setFileAccessControl(serverSession, this, file, acl, options); } protected void handleUnsupportedAttributes(Collection<String> attributes) { @@ -2798,32 +2770,21 @@ public abstract class AbstractSftpSubsystemHelper } 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); + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + return accessor.resolveGroupOwner(getServerSession(), this, file, name); } catch (IOException e) { - handleUserPrincipalLookupServiceException(GroupPrincipal.class, groupName, e); + handleUserPrincipalLookupServiceException(GroupPrincipal.class, name.toString(), 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); + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + return accessor.resolveFileOwner(getServerSession(), this, file, name); } catch (IOException e) { - handleUserPrincipalLookupServiceException(UserPrincipal.class, username, e); + handleUserPrincipalLookupServiceException(UserPrincipal.class, name.toString(), e); return null; } } @@ -2954,13 +2915,13 @@ public abstract class AbstractSftpSubsystemHelper */ protected Path resolveFile(String remotePath) throws IOException, InvalidPathException { - Path defaultDir = getDefaultDirectory(); - String path = SelectorUtils.translateToLocalFileSystemPath( - remotePath, '/', defaultDir.getFileSystem()); - Path p = defaultDir.resolve(path); + ServerSession serverSession = getServerSession(); + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + Path localPath = accessor.resolveLocalFilePath( + serverSession, this, getDefaultDirectory(), remotePath); if (log.isTraceEnabled()) { - log.trace("resolveFile({}) {} => {}", getServerSession(), remotePath, p); + log.trace("resolveFile({}) {} => {}", serverSession, remotePath, localPath); } - return p; + return localPath; } } diff --git a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpFileSystemAccessor.java b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpFileSystemAccessor.java index 80a1432..cf98d39 100644 --- a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpFileSystemAccessor.java +++ b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpFileSystemAccessor.java @@ -25,19 +25,37 @@ import java.nio.channels.Channel; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.SeekableByteChannel; +import java.nio.file.CopyOption; import java.nio.file.DirectoryStream; +import java.nio.file.FileSystem; import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.LinkOption; import java.nio.file.OpenOption; import java.nio.file.Path; +import java.nio.file.attribute.AclEntry; +import java.nio.file.attribute.AclFileAttributeView; import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileOwnerAttributeView; +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.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.NavigableMap; import java.util.Set; import org.apache.sshd.common.PropertyResolverUtils; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.MapEntryUtils.NavigableMapBuilder; +import org.apache.sshd.common.util.OsUtils; +import org.apache.sshd.common.util.SelectorUtils; import org.apache.sshd.common.util.io.FileInfoExtractor; import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.server.session.ServerSession; @@ -77,13 +95,37 @@ public interface SftpFileSystemAccessor { }; /** + * Invoked in order to resolve remote file paths reference by the client into ones accessible by the server + * + * @param session The {@link ServerSession} through which the request was received + * @param subsystem The SFTP subsystem instance that manages the session + * @param rootDir The default root directory used to resolve relative paths - a.k.a. the + * {@code chroot} location + * @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 + * @see org.apache.sshd.server.subsystem.sftp.SftpSubsystemEnvironment#getDefaultDirectory() + * SftpSubsystemEnvironment#getDefaultDirectory() + */ + default Path resolveLocalFilePath( + ServerSession session, SftpSubsystemProxy subsystem, Path rootDir, String remotePath) + throws IOException, InvalidPathException { + String path = SelectorUtils.translateToLocalFileSystemPath( + remotePath, '/', rootDir.getFileSystem()); + return rootDir.resolve(path); + } + + /** * Called whenever a new file is opened * * @param session The {@link ServerSession} through which the request was received * @param subsystem The SFTP subsystem instance that manages the session * @param fileHandle The {@link FileHandle} representing the created channel - may be {@code null} if not invoked * within the context of such a handle (special cases) - * @param file The requested <U>local</U> file {@link Path} + * @param file The requested <U>local</U> file {@link Path} - same one returned by + * {@link #resolveLocalFilePath(ServerSession, SftpSubsystemProxy, Path, String) + * resolveLocalFilePath} * @param handle The assigned file handle through which the remote peer references this file. May be * {@code null}/empty if the request is due to some internal functionality instead of due to * peer requesting a handle to a file. @@ -93,7 +135,7 @@ public interface SftpFileSystemAccessor { * @throws IOException If failed to open */ default SeekableByteChannel openFile( - ServerSession session, SftpEventListenerManager subsystem, FileHandle fileHandle, + ServerSession session, SftpSubsystemProxy subsystem, FileHandle fileHandle, Path file, String handle, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException { /* @@ -114,10 +156,12 @@ public interface SftpFileSystemAccessor { * @param session The {@link ServerSession} through which the request was received * @param subsystem The SFTP subsystem instance that manages the session * @param fileHandle The {@link FileHandle} representing the created channel - * @param file The requested <U>local</U> file {@link Path} + * @param file The requested <U>local</U> file {@link Path} - same one returned by + * {@link #resolveLocalFilePath(ServerSession, SftpSubsystemProxy, Path, String) + * resolveLocalFilePath} * @param handle The assigned file handle through which the remote peer references this file * @param channel The original {@link Channel} that was returned by - * {@link #openFile(ServerSession, SftpEventListenerManager, FileHandle, Path, String, Set, FileAttribute...)} + * {@link #openFile(ServerSession, SftpSubsystemProxy, FileHandle, Path, String, Set, FileAttribute...)} * @param position The position at which the locked region is to start - must be non-negative * @param size The size of the locked region; must be non-negative, and the sum * <tt>position</tt> + <tt>size</tt> must be non-negative @@ -129,7 +173,7 @@ public interface SftpFileSystemAccessor { */ @SuppressWarnings("checkstyle:ParameterNumber") default FileLock tryLock( - ServerSession session, SftpEventListenerManager subsystem, FileHandle fileHandle, + ServerSession session, SftpSubsystemProxy subsystem, FileHandle fileHandle, Path file, String handle, Channel channel, long position, long size, boolean shared) throws IOException { if (!(channel instanceof FileChannel)) { @@ -145,17 +189,19 @@ public interface SftpFileSystemAccessor { * @param session The {@link ServerSession} through which the request was received * @param subsystem The SFTP subsystem instance that manages the session * @param fileHandle The {@link FileHandle} representing the created channel - * @param file The requested <U>local</U> file {@link Path} + * @param file The requested <U>local</U> file {@link Path} - same one returned by + * {@link #resolveLocalFilePath(ServerSession, SftpSubsystemProxy, Path, String) + * resolveLocalFilePath} * @param handle The assigned file handle through which the remote peer references this file * @param channel The original {@link Channel} that was returned by - * {@link #openFile(ServerSession, SftpEventListenerManager, FileHandle, Path, String, Set, FileAttribute...)} + * {@link #openFile(ServerSession, SftpSubsystemProxy, FileHandle, Path, String, Set, FileAttribute...)} * @throws IOException If failed to execute the request * @see FileChannel#force(boolean) * @see <A HREF="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL">OpenSSH - section * 10</A> */ default void syncFileData( - ServerSession session, SftpEventListenerManager subsystem, FileHandle fileHandle, Path file, String handle, + ServerSession session, SftpSubsystemProxy subsystem, FileHandle fileHandle, Path file, String handle, Channel channel) throws IOException { if (!(channel instanceof FileChannel)) { @@ -172,15 +218,17 @@ public interface SftpFileSystemAccessor { * @param subsystem The SFTP subsystem instance that manages the session * @param fileHandle The {@link FileHandle} representing the created channel - may be {@code null} if not invoked * within the context of such a handle (special cases) - * @param file The requested <U>local</U> file {@link Path} + * @param file The requested <U>local</U> file {@link Path} - same one returned by + * {@link #resolveLocalFilePath(ServerSession, SftpSubsystemProxy, Path, String) + * resolveLocalFilePath} * @param handle The assigned file handle through which the remote peer references this file * @param channel The original {@link Channel} that was returned by - * {@link #openFile(ServerSession, SftpEventListenerManager, FileHandle, Path, String, Set, FileAttribute...)} + * {@link #openFile(ServerSession, SftpSubsystemProxy, FileHandle, Path, String, Set, FileAttribute...)} * @param options The original options used to open the channel * @throws IOException If failed to execute the request */ default void closeFile( - ServerSession session, SftpEventListenerManager subsystem, FileHandle fileHandle, + ServerSession session, SftpSubsystemProxy subsystem, FileHandle fileHandle, Path file, String handle, Channel channel, Set<? extends OpenOption> options) throws IOException { if ((channel == null) || (!channel.isOpen())) { @@ -203,13 +251,15 @@ public interface SftpFileSystemAccessor { * @param session The {@link ServerSession} through which the request was received * @param subsystem The SFTP subsystem instance that manages the session * @param dirHandle The {@link DirectoryHandle} representing the stream - * @param dir The requested <U>local</U> directory + * @param dir The requested <U>local</U> directory {@link Path} - same one returned by + * {@link #resolveLocalFilePath(ServerSession, SftpSubsystemProxy, Path, String) + * resolveLocalFilePath} * @param handle The assigned directory handle through which the remote peer references this directory * @return The opened {@link DirectoryStream} * @throws IOException If failed to open */ default DirectoryStream<Path> openDirectory( - ServerSession session, SftpEventListenerManager subsystem, DirectoryHandle dirHandle, Path dir, String handle) + ServerSession session, SftpSubsystemProxy subsystem, DirectoryHandle dirHandle, Path dir, String handle) throws IOException { return Files.newDirectoryStream(dir); } @@ -221,13 +271,15 @@ public interface SftpFileSystemAccessor { * @param subsystem The SFTP subsystem instance that manages the session * @param dirHandle The {@link DirectoryHandle} representing the stream - may be {@code null} if not invoked * within the context of such a handle (special cases) - * @param dir The requested <U>local</U> directory + * @param dir The requested <U>local</U> directory {@link Path} - same one returned by + * {@link #resolveLocalFilePath(ServerSession, SftpSubsystemProxy, Path, String) + * resolveLocalFilePath} * @param handle The assigned directory handle through which the remote peer references this directory * @param ds The disposed {@link DirectoryStream} * @throws IOException If failed to open */ default void closeDirectory( - ServerSession session, SftpEventListenerManager subsystem, DirectoryHandle dirHandle, + ServerSession session, SftpSubsystemProxy subsystem, DirectoryHandle dirHandle, Path dir, String handle, DirectoryStream<Path> ds) throws IOException { if (ds == null) { @@ -236,4 +288,211 @@ public interface SftpFileSystemAccessor { ds.close(); } + + /** + * Invoked when required to retrieve file attributes for a specific file system view + * + * @param session The {@link ServerSession} through which the request was received + * @param subsystem The SFTP subsystem instance that manages the session + * @param file The requested <U>local</U> file {@link Path} - same one returned by + * {@link #resolveLocalFilePath(ServerSession, SftpSubsystemProxy, Path, String) + * resolveLocalFilePath} + * @param view The required view name + * @param options The access {@link LinkOption}-s + * @return A {@link Map} of all the attributes available for the file in the view + * @throws IOException If failed to read the attributes + * @see Files#readAttributes(Path, String, LinkOption...) + */ + default Map<String, ?> readFileAttributes( + ServerSession session, SftpSubsystemProxy subsystem, + Path file, String view, LinkOption... options) + throws IOException { + return Files.readAttributes(file, view, options); + } + + /** + * Sets a view attribute for a local file + * + * @param session The {@link ServerSession} through which the request was received + * @param subsystem The SFTP subsystem instance that manages the session + * @param file The requested <U>local</U> file {@link Path} - same one returned by + * {@link #resolveLocalFilePath(ServerSession, SftpSubsystemProxy, Path, String) + * resolveLocalFilePath} + * @param view The required view name + * @param attribute The attribute name + * @param value The attribute value + * @param options The access {@link LinkOption}-s + * @throws IOException If failed to set the attribute + */ + default void setFileAttribute( + ServerSession session, SftpSubsystemProxy subsystem, Path file, + String view, String attribute, Object value, LinkOption... options) + throws IOException { + if (value == null) { + return; + } + + Files.setAttribute(file, view + ":" + attribute, value, options); + } + + default UserPrincipal resolveFileOwner( + ServerSession session, SftpSubsystemProxy subsystem, Path file, UserPrincipal name) + throws IOException { + FileSystem fileSystem = file.getFileSystem(); + UserPrincipalLookupService lookupService = fileSystem.getUserPrincipalLookupService(); + String username = name.toString(); + + if (lookupService == null) { + throw new UserPrincipalNotFoundException(username); + } + + return lookupService.lookupPrincipalByName(username); + } + + default void setFileOwner( + ServerSession session, SftpSubsystemProxy subsystem, Path file, + Principal value, LinkOption... options) + throws IOException { + if (value == null) { + return; + } + + 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); + } + + default GroupPrincipal resolveGroupOwner( + ServerSession session, SftpSubsystemProxy subsystem, Path file, GroupPrincipal name) + throws IOException { + FileSystem fileSystem = file.getFileSystem(); + UserPrincipalLookupService lookupService = fileSystem.getUserPrincipalLookupService(); + String groupName = name.toString(); + if (lookupService == null) { + throw new UserPrincipalNotFoundException(groupName); + } + return lookupService.lookupPrincipalByGroupName(groupName); + + } + + default void setGroupOwner( + ServerSession session, SftpSubsystemProxy subsystem, + Path file, Principal value, LinkOption... options) + throws IOException { + if (value == null) { + return; + } + + 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); + } + + default void setFilePermissions( + ServerSession session, SftpSubsystemProxy subsystem, 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); + } + + view.setPermissions(perms); + } + + default void setFileAccessControl( + ServerSession session, SftpSubsystemProxy subsystem, + 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); + } + + view.setAcl(acl); + } + + default void createDirectory( + ServerSession session, SftpSubsystemProxy subsystem, Path path) + throws IOException { + Files.createDirectory(path); + } + + /** + * Invoked in order to create a link to a path + * + * @param session The {@link ServerSession} through which the request was received + * @param subsystem The SFTP subsystem instance that manages the session + * @param link The requested <U>link</U> {@link Path} - same one returned by + * {@link #resolveLocalFilePath(ServerSession, SftpSubsystemProxy, Path, String) + * resolveLocalFilePath} + * @param existing The <U>existing</U> {@link Path} that the link should reference + * @param symLink {@code true} if this should be a symbolic link + * @throws IOException If failed to create the link + * @see Files#createLink(Path, Path) + * @see Files#createSymbolicLink(Path, Path, FileAttribute...) + */ + default void createLink( + ServerSession session, SftpSubsystemProxy subsystem, Path link, Path existing, boolean symLink) + throws IOException { + if (symLink) { + Files.createSymbolicLink(link, existing); + } else { + Files.createLink(link, existing); + } + } + + default String resolveLinkTarget( + ServerSession session, SftpSubsystemProxy subsystem, Path link) + throws IOException { + Path target = Files.readSymbolicLink(link); + return target.toString(); + } + + default void renameFile( + ServerSession session, SftpSubsystemProxy subsystem, + Path oldPath, Path newPath, Collection<CopyOption> opts) + throws IOException { + Files.move(oldPath, newPath, + GenericUtils.isEmpty(opts) + ? IoUtils.EMPTY_COPY_OPTIONS + : opts.toArray(new CopyOption[opts.size()])); + } + + default void copyFile( + ServerSession session, SftpSubsystemProxy subsystem, + Path src, Path dst, Collection<CopyOption> opts) + throws IOException { + Files.copy(src, dst, + GenericUtils.isEmpty(opts) + ? IoUtils.EMPTY_COPY_OPTIONS + : opts.toArray(new CopyOption[opts.size()])); + } + + default void removeFile( + ServerSession session, SftpSubsystemProxy subsystem, Path path, boolean isDirectory) + throws IOException { + Files.delete(path); + } } diff --git a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java index 643f02b..4415883 100644 --- a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java +++ b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java @@ -362,11 +362,8 @@ public class SftpSubsystem SftpEventListener listener = getSftpEventListenerProxy(); listener.linking(session, link, existing, symLink); try { - if (symLink) { - Files.createSymbolicLink(link, existing); - } else { - Files.createLink(link, existing); - } + SftpFileSystemAccessor accessor = getFileSystemAccessor(); + accessor.createLink(session, this, link, existing, symLink); } catch (IOException | RuntimeException e) { listener.linked(session, link, existing, symLink, e); throw e; diff --git a/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystemProxy.java b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystemProxy.java new file mode 100644 index 0000000..d224dfc --- /dev/null +++ b/sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystemProxy.java @@ -0,0 +1,29 @@ +/* + * 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; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public interface SftpSubsystemProxy + extends SftpSubsystemEnvironment, + SftpEventListenerManager { + // Nothing extra +} diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java index 3078ec2..d5484ad 100644 --- a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java +++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java @@ -100,10 +100,10 @@ import org.apache.sshd.server.subsystem.sftp.DirectoryHandle; import org.apache.sshd.server.subsystem.sftp.FileHandle; import org.apache.sshd.server.subsystem.sftp.Handle; import org.apache.sshd.server.subsystem.sftp.SftpEventListener; -import org.apache.sshd.server.subsystem.sftp.SftpEventListenerManager; import org.apache.sshd.server.subsystem.sftp.SftpFileSystemAccessor; import org.apache.sshd.server.subsystem.sftp.SftpSubsystemEnvironment; import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; +import org.apache.sshd.server.subsystem.sftp.SftpSubsystemProxy; import org.apache.sshd.util.test.CommonTestSupportUtils; import org.apache.sshd.util.test.SimpleUserInfo; import org.junit.After; @@ -629,7 +629,7 @@ public class SftpTest extends AbstractSftpClientTestSupport { factory.setFileSystemAccessor(new SftpFileSystemAccessor() { @Override public SeekableByteChannel openFile( - ServerSession session, SftpEventListenerManager subsystem, FileHandle fileHandle, Path file, + ServerSession session, SftpSubsystemProxy subsystem, FileHandle fileHandle, Path file, String handle, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException { fileHolder.set(file); @@ -639,7 +639,7 @@ public class SftpTest extends AbstractSftpClientTestSupport { @Override public DirectoryStream<Path> openDirectory( - ServerSession session, SftpEventListenerManager subsystem, + ServerSession session, SftpSubsystemProxy subsystem, DirectoryHandle dirHandle, Path dir, String handle) throws IOException { dirHolder.set(dir);