I'm working on a project where I need to set up an SFTP server using Apache SSHD, which allows users to access multiple directories within a specified root directory. The root directory should be restricted to C:/FTP, and users should only be able to see and access their designated subdirectories within this root directory.
Despite setting the C:/FTP as the root directory and specifying the user directories, users are seeing the entire C:/ drive instead of being restricted to C:/FTP and their specific directories.
Actual behavior
Current Setup
I have implemented a custom SFTP server using Apache SSHD, and it successfully authenticates users. However, I'm facing issues with restricting the user's view to their specific directories. Instead of seeing their designated directories within C:/FTP, users can see the entire C:/ drive.
Here's the code I am using:
public class MySftpServer {
private static final Log log = LogFactory.getLog(MySftpServer.class);
private static final AttributeKey<UserManager.User> USER_KEY = new AttributeKey<>();
@PostConstruct
public void startServer() throws IOException {
start();
}
private void start() throws IOException {
SshServer sshd = SshServer.setUpDefaultServer();
sshd.setHost("localhost");
sshd.setPort(2222);
sshd.setKeyPairProvider(new PEMKeyPairProvider("src/main/resources/host_key.ser"));
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
sshd.setPasswordAuthenticator(new PasswordAuthenticator() {
@Override
public boolean authenticate(String username, String password, ServerSession session) {
UserManager.User user = UserManager.authenticate(username, password);
if (user != null) {
session.setAttribute(USER_KEY, user);
return true;
}
return false;
}
});
sshd.setPublickeyAuthenticator(new AuthorizedKeysAuthenticator(Paths.get("src/main/resources/authorized_keys")));
// Set idle timeout to 30 minutes
sshd.getProperties().put(SshServer.IDLE_TIMEOUT, TimeUnit.MINUTES.toMillis(30));
// Set authentication timeout to 2 minutes
sshd.getProperties().put(SshServer.AUTH_TIMEOUT, TimeUnit.MINUTES.toMillis(2));
// Set up the custom file system factory with user home directories
sshd.setFileSystemFactory(new CustomFileSystemFactory(Paths.get("C:/FTP")));
sshd.start();
log.info("SFTP server started");
}
public static class CustomFileSystemFactory implements FileSystemFactory {
private final Path rootDir;
public CustomFileSystemFactory(Path rootDir) {
this.rootDir = rootDir;
}
@Override
public FileSystem createFileSystem(Session session) throws IOException {
UserManager.User user = session.getAttribute(USER_KEY);
if (user == null) {
throw new IOException("No user found in session");
}
List<Path> userDirs = user.getDirectories().stream()
.map(dir -> rootDir.resolve(dir).toAbsolutePath().normalize())
.collect(Collectors.toList());
return new MultiDirectoryFileSystem(userDirs);
}
}
public static class MultiDirectoryFileSystem extends FileSystem {
private final List<Path> roots;
public MultiDirectoryFileSystem(List<Path> directories) {
this.roots = directories;
}
@Override
public FileSystemProvider provider() {
return new MultiDirectoryFileSystemProvider(roots);
}
@Override
public void close() throws IOException {
// Implement close logic if needed
}
@Override
public boolean isOpen() {
return true;
}
@Override
public boolean isReadOnly() {
return false;
}
@Override
public String getSeparator() {
return FileSystems.getDefault().getSeparator();
}
@Override
public Iterable<Path> getRootDirectories() {
return roots;
}
@Override
public Iterable<FileStore> getFileStores() {
return Collections.emptyList();
}
@Override
public Set<String> supportedFileAttributeViews() {
return FileSystems.getDefault().supportedFileAttributeViews();
}
@Override
public Path getPath(String first, String... more) {
Path path = FileSystems.getDefault().getPath(first, more).toAbsolutePath().normalize();
for (Path root : roots) {
if (path.startsWith(root)) {
return path;
}
}
throw new IllegalArgumentException("Path is not under any root: " + path);
}
@Override
public PathMatcher getPathMatcher(String syntaxAndPattern) {
return FileSystems.getDefault().getPathMatcher(syntaxAndPattern);
}
@Override
public UserPrincipalLookupService getUserPrincipalLookupService() {
return FileSystems.getDefault().getUserPrincipalLookupService();
}
@Override
public WatchService newWatchService() throws IOException {
return FileSystems.getDefault().newWatchService();
}
}
public static class MultiDirectoryFileSystemProvider extends FileSystemProvider {
private final List<Path> roots;
public MultiDirectoryFileSystemProvider(List<Path> roots) {
this.roots = roots;
}
@Override
public String getScheme() {
return "multi-dir";
}
@Override
public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
throw new UnsupportedOperationException("Creating new file systems is not supported.");
}
@Override
public FileSystem getFileSystem(URI uri) {
throw new UnsupportedOperationException("Getting file system by URI is not supported.");
}
@Override
public Path getPath(URI uri) {
return Paths.get(uri).toAbsolutePath().normalize();
}
@Override
public FileChannel newFileChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
validateAccess(path);
return FileSystems.getDefault().provider().newFileChannel(path, options, attrs);
}
@Override
public AsynchronousFileChannel newAsynchronousFileChannel(Path path, Set<? extends OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) throws IOException {
validateAccess(path);
return FileSystems.getDefault().provider().newAsynchronousFileChannel(path, options, executor, attrs);
}
@Override
public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
validateAccess(dir);
return FileSystems.getDefault().provider().newDirectoryStream(dir, filter);
}
@Override
public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
validateAccess(dir);
FileSystems.getDefault().provider().createDirectory(dir, attrs);
}
@Override
public void delete(Path path) throws IOException {
validateAccess(path);
FileSystems.getDefault().provider().delete(path);
}
@Override
public void copy(Path source, Path target, CopyOption... options) throws IOException {
validateAccess(source);
validateAccess(target);
FileSystems.getDefault().provider().copy(source, target, options);
}
@Override
public void move(Path source, Path target, CopyOption... options) throws IOException {
validateAccess(source);
validateAccess(target);
FileSystems.getDefault().provider().move(source, target, options);
}
@Override
public boolean isSameFile(Path path, Path path2) throws IOException {
validateAccess(path);
validateAccess(path2);
return FileSystems.getDefault().provider().isSameFile(path, path2);
}
@Override
public boolean isHidden(Path path) throws IOException {
validateAccess(path);
return FileSystems.getDefault().provider().isHidden(path);
}
@Override
public FileStore getFileStore(Path path) throws IOException {
validateAccess(path);
return FileSystems.getDefault().provider().getFileStore(path);
}
@Override
public void checkAccess(Path path, AccessMode... modes) throws IOException {
validateAccess(path);
FileSystems.getDefault().provider().checkAccess(path, modes);
}
@Override
public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
try {
validateAccess(path);
} catch (IOException e) {
e.printStackTrace();
}
return FileSystems.getDefault().provider().getFileAttributeView(path, type, options);
}
@Override
public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException {
validateAccess(path);
return FileSystems.getDefault().provider().readAttributes(path, type, options);
}
@Override
public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
validateAccess(path);
return FileSystems.getDefault().provider().readAttributes(path, attributes, options);
}
@Override
public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
validateAccess(path);
FileSystems.getDefault().provider().setAttribute(path, attribute, value, options);
}
private void validateAccess(Path path) throws IOException {
for (Path root : roots) {
if (path.startsWith(root)) {
return;
}
}
throw new IOException("Access denied to path: " + path);
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options,
FileAttribute<?>... attrs) throws IOException {
validateAccess(path);
return FileSystems.getDefault().provider().newByteChannel(path, options, attrs);
}
}
public static class PEMKeyPairProvider implements KeyPairProvider {
private final KeyPair keyPair;
public PEMKeyPairProvider(String privateKeyPath) throws IOException {
try (PEMParser pemParser = new PEMParser(new FileReader(privateKeyPath))) {
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
Object object = pemParser.readObject();
if (object instanceof PEMKeyPair) {
this.keyPair = converter.getKeyPair((PEMKeyPair) object);
} else if (object instanceof PrivateKeyInfo) {
PrivateKey privateKey = converter.getPrivateKey((PrivateKeyInfo) object);
SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemParser.readObject());
PublicKey publicKey = converter.getPublicKey(publicKeyInfo);
this.keyPair = new KeyPair(publicKey, privateKey);
} else {
throw new IllegalArgumentException("Invalid key format");
}
}
}
@Override
public Iterable<KeyPair> loadKeys() {
return Collections.singletonList(keyPair);
}
}
}
Expected behavior
The SFTP root directory should be C:/FTP.
Users should only see and access their designated directories within C:/FTP.
For example, user1 should only see C:/FTP/001 and C:/FTP/002.
Version
1.7.0
Bug description
Hello everyone,
I'm working on a project where I need to set up an SFTP server using Apache SSHD, which allows users to access multiple directories within a specified root directory. The root directory should be restricted to C:/FTP, and users should only be able to see and access their designated subdirectories within this root directory.
Despite setting the C:/FTP as the root directory and specifying the user directories, users are seeing the entire C:/ drive instead of being restricted to C:/FTP and their specific directories.
Actual behavior
Current Setup I have implemented a custom SFTP server using Apache SSHD, and it successfully authenticates users. However, I'm facing issues with restricting the user's view to their specific directories. Instead of seeing their designated directories within C:/FTP, users can see the entire C:/ drive.
Here's the code I am using:
public class MySftpServer {
}
Expected behavior
The SFTP root directory should be C:/FTP. Users should only see and access their designated directories within C:/FTP. For example, user1 should only see C:/FTP/001 and C:/FTP/002.
Relevant log output
No response
Other information
No response