VYPR
Moderate severityNVD Advisory· Published Jul 10, 2023· Updated Oct 7, 2024

Apache MINA SSHD: Information disclosure bugs with RootedFilesystem

CVE-2023-35887

Description

Apache MINA SSHD RootedFileSystem allows authenticated users to discover existence of files outside the chroot via parent path traversal or symlinks.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Apache MINA SSHD RootedFileSystem allows authenticated users to discover existence of files outside the chroot via parent path traversal or symlinks.

Vulnerability

Description

CVE-2023-35887 is an information disclosure vulnerability in Apache MINA SSHD when using a RootedFileSystem for SFTP servers. The root cause is that the implementation does not properly restrict path resolution containing parent directory references (..) or symlinks, allowing a logged-in user to probe for the existence of files or directories outside the designated root [1][2].

Exploitation

An attacker must have valid credentials to log into the SFTP server. Once authenticated, they can craft SFTP requests that include .. path components or follow symlinks that point outside the rooted tree. The server responds differently depending on whether the target resource exists or not, effectively leaking a boolean "exists/does not exist" for arbitrary paths on the underlying filesystem [1][2].

Impact

The vulnerability only reveals existence information; no file contents are exposed. However, this reconnaissance can aid further attacks by mapping the filesystem layout, identifying sensitive files or configuration paths, and potentially bypassing access controls in multi-tenant environments [1][2].

Mitigation

The issue affects Apache MINA SSHD versions from 1.0 before 2.10. Users should upgrade to version 2.10 or later, which contains the fix implemented in commits [3][4]. No workarounds are documented; upgrading is the recommended course of action.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.apache.sshd:sshd-commonMaven
>= 2.1.0, < 2.9.32.9.3
org.apache.sshd:sshd-sftpMaven
>= 1.0.0, < 2.9.32.9.3
org.apache.sshd:sshd-coreMaven
>= 1.0.0, < 2.1.02.1.0

Affected products

5

Patches

3
a61e93035f06

[SSHD-1234] Rooted file system can leak informations

https://github.com/apache/mina-sshdGuillaume NodetMay 9, 2023via ghsa
16 files changed · +1203 269
  • sshd-common/src/main/java/org/apache/sshd/common/file/root/RootedDirectoryStream.java+63 0 added
    @@ -0,0 +1,63 @@
    +/*
    + * 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.common.file.root;
    +
    +import java.io.IOException;
    +import java.nio.file.DirectoryStream;
    +import java.nio.file.Path;
    +import java.util.Iterator;
    +
    +/**
    + * secure directory stream proxy for a {@link RootedFileSystem}
    + *
    + * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
    + */
    +public class RootedDirectoryStream implements DirectoryStream<Path> {
    +    protected final RootedFileSystem rfs;
    +    protected final DirectoryStream<Path> delegate;
    +
    +    public RootedDirectoryStream(RootedFileSystem rfs, DirectoryStream<Path> delegate) {
    +        this.rfs = rfs;
    +        this.delegate = delegate;
    +    }
    +
    +    @Override
    +    public Iterator<Path> iterator() {
    +        return root(rfs, delegate.iterator());
    +    }
    +
    +    @Override
    +    public void close() throws IOException {
    +        delegate.close();
    +    }
    +
    +    protected Iterator<Path> root(RootedFileSystem rfs, Iterator<Path> iter) {
    +        return new Iterator<Path>() {
    +            @Override
    +            public boolean hasNext() {
    +                return iter.hasNext();
    +            }
    +
    +            @Override
    +            public Path next() {
    +                return rfs.provider().root(rfs, iter.next());
    +            }
    +        };
    +    }
    +}
    
  • sshd-common/src/main/java/org/apache/sshd/common/file/root/RootedFileSystem.java+5 0 modified
    @@ -94,6 +94,11 @@ public Iterable<FileStore> getFileStores() {
             return rootFs.getFileStores();
         }
     
    +    @Override
    +    protected boolean hostFsHasWindowsSeparator() {
    +        return "\\".equals(getRoot().getFileSystem().getSeparator());
    +    }
    +
         @Override
         public String toString() {
             return rootPath.toString();
    
  • sshd-common/src/main/java/org/apache/sshd/common/file/root/RootedFileSystemProvider.java+225 102 modified
    @@ -18,6 +18,7 @@
      */
     package org.apache.sshd.common.file.root;
     
    +import java.io.FileNotFoundException;
     import java.io.IOException;
     import java.io.InputStream;
     import java.io.OutputStream;
    @@ -26,32 +27,40 @@
     import java.nio.channels.AsynchronousFileChannel;
     import java.nio.channels.FileChannel;
     import java.nio.channels.SeekableByteChannel;
    +import java.nio.file.AccessDeniedException;
     import java.nio.file.AccessMode;
    +import java.nio.file.AtomicMoveNotSupportedException;
     import java.nio.file.CopyOption;
    +import java.nio.file.DirectoryNotEmptyException;
     import java.nio.file.DirectoryStream;
    +import java.nio.file.FileAlreadyExistsException;
     import java.nio.file.FileStore;
     import java.nio.file.FileSystem;
     import java.nio.file.FileSystemAlreadyExistsException;
    +import java.nio.file.FileSystemException;
    +import java.nio.file.FileSystemLoopException;
     import java.nio.file.FileSystemNotFoundException;
     import java.nio.file.Files;
     import java.nio.file.InvalidPathException;
     import java.nio.file.LinkOption;
    +import java.nio.file.NoSuchFileException;
    +import java.nio.file.NotDirectoryException;
    +import java.nio.file.NotLinkException;
     import java.nio.file.OpenOption;
     import java.nio.file.Path;
     import java.nio.file.Paths;
     import java.nio.file.ProviderMismatchException;
    +import java.nio.file.SecureDirectoryStream;
     import java.nio.file.attribute.BasicFileAttributes;
     import java.nio.file.attribute.FileAttribute;
     import java.nio.file.attribute.FileAttributeView;
     import java.nio.file.spi.FileSystemProvider;
     import java.util.HashMap;
    -import java.util.Iterator;
     import java.util.Map;
     import java.util.Objects;
     import java.util.Set;
     import java.util.concurrent.ExecutorService;
     
    -import org.apache.sshd.common.util.ValidateUtils;
     import org.apache.sshd.common.util.io.IoUtils;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
    @@ -157,22 +166,34 @@ public Path getPath(URI uri) {
         public InputStream newInputStream(Path path, OpenOption... options) throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        return p.newInputStream(r, options);
    +        try {
    +            return p.newInputStream(r, options);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public OutputStream newOutputStream(Path path, OpenOption... options) throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        return p.newOutputStream(r, options);
    +        try {
    +            return p.newOutputStream(r, options);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public FileChannel newFileChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
                 throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        return p.newFileChannel(r, options, attrs);
    +        try {
    +            return p.newFileChannel(r, options, attrs);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
    @@ -181,90 +202,86 @@ public AsynchronousFileChannel newAsynchronousFileChannel(
                 throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        return p.newAsynchronousFileChannel(r, options, executor, attrs);
    +        try {
    +            return p.newAsynchronousFileChannel(r, options, executor, attrs);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
                 throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        return p.newByteChannel(r, options, attrs);
    +        try {
    +            return p.newByteChannel(r, options, attrs);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
             Path r = unroot(dir);
             FileSystemProvider p = provider(r);
    -        return root(((RootedPath) dir).getFileSystem(), p.newDirectoryStream(r, filter));
    +        try {
    +            return root(((RootedPath) dir).getFileSystem(), p.newDirectoryStream(r, filter));
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, dir);
    +        }
         }
     
         protected DirectoryStream<Path> root(RootedFileSystem rfs, DirectoryStream<Path> ds) {
    -        return new DirectoryStream<Path>() {
    -            @Override
    -            public Iterator<Path> iterator() {
    -                return root(rfs, ds.iterator());
    -            }
    -
    -            @Override
    -            public void close() throws IOException {
    -                ds.close();
    -            }
    -        };
    -    }
    -
    -    protected Iterator<Path> root(RootedFileSystem rfs, Iterator<Path> iter) {
    -        return new Iterator<Path>() {
    -            @Override
    -            public boolean hasNext() {
    -                return iter.hasNext();
    -            }
    -
    -            @Override
    -            public Path next() {
    -                return root(rfs, iter.next());
    -            }
    -        };
    +        if (ds instanceof SecureDirectoryStream) {
    +            return new RootedSecureDirectoryStream(rfs, (SecureDirectoryStream<Path>) ds);
    +        }
    +        return new RootedDirectoryStream(rfs, ds);
         }
     
         @Override
         public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
             Path r = unroot(dir);
             FileSystemProvider p = provider(r);
    -        p.createDirectory(r, attrs);
    +        try {
    +            p.createDirectory(r, attrs);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, dir);
    +        }
         }
     
         @Override
         public void createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs) throws IOException {
    -        createLink(link, target, true, attrs);
    -    }
    -
    -    @Override
    -    public void createLink(Path link, Path existing) throws IOException {
    -        createLink(link, existing, false);
    -    }
    -
    -    protected void createLink(Path link, Path target, boolean symLink, FileAttribute<?>... attrs) throws IOException {
    +        // make sure symlink cannot break out of chroot jail. If it is unsafe, simply thrown an exception. This is
    +        // to ensure that symlink semantics are maintained when it is safe, and creation fails when not.
    +        RootedFileSystemUtils.validateSafeRelativeSymlink(target);
             Path l = unroot(link);
    -        Path t = unroot(target);
    -        /*
    -         * For a symbolic link preserve the relative path
    -         */
    -        if (symLink && (!target.isAbsolute())) {
    -            RootedFileSystem rfs = ((RootedPath) target).getFileSystem();
    -            Path root = rfs.getRoot();
    -            t = root.relativize(t);
    -        }
    +        Path t = target.isAbsolute() ? unroot(target) : l.getFileSystem().getPath(target.toString());
     
             FileSystemProvider p = provider(l);
    -        if (symLink) {
    +        try {
                 p.createSymbolicLink(l, t, attrs);
    -        } else {
    -            p.createLink(l, t);
    +
    +            if (log.isDebugEnabled()) {
    +                log.debug("createSymbolicLink({} => {}", l, t);
    +            }
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, link);
             }
    +    }
     
    -        if (log.isDebugEnabled()) {
    -            log.debug("createLink(symbolic={}) {} => {}", symLink, l, t);
    +    @Override
    +    public void createLink(Path link, Path existing) throws IOException {
    +        Path l = unroot(link);
    +        Path t = unroot(existing);
    +
    +        try {
    +            provider(l).createLink(l, t);
    +            if (log.isDebugEnabled()) {
    +                log.debug("createLink({} => {}", l, t);
    +            }
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, link);
             }
         }
     
    @@ -275,7 +292,11 @@ public void delete(Path path) throws IOException {
                 log.trace("delete({}): {}", path, r);
             }
             FileSystemProvider p = provider(r);
    -        p.delete(r);
    +        try {
    +            p.delete(r);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
    @@ -285,19 +306,27 @@ public boolean deleteIfExists(Path path) throws IOException {
                 log.trace("deleteIfExists({}): {}", path, r);
             }
             FileSystemProvider p = provider(r);
    -        return p.deleteIfExists(r);
    +        try {
    +            return p.deleteIfExists(r);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public Path readSymbolicLink(Path link) throws IOException {
             Path r = unroot(link);
             FileSystemProvider p = provider(r);
    -        Path t = p.readSymbolicLink(r);
    -        Path target = root((RootedFileSystem) link.getFileSystem(), t);
    -        if (log.isTraceEnabled()) {
    -            log.trace("readSymbolicLink({})[{}]: {}[{}]", link, r, target, t);
    +        try {
    +            Path t = p.readSymbolicLink(r);
    +            Path target = root((RootedFileSystem) link.getFileSystem(), t);
    +            if (log.isTraceEnabled()) {
    +                log.trace("readSymbolicLink({})[{}]: {}[{}]", link, r, target, t);
    +            }
    +            return target;
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, link);
             }
    -        return target;
         }
     
         @Override
    @@ -308,7 +337,11 @@ public void copy(Path source, Path target, CopyOption... options) throws IOExcep
                 log.trace("copy({})[{}]: {}[{}]", source, s, target, t);
             }
             FileSystemProvider p = provider(s);
    -        p.copy(s, t, options);
    +        try {
    +            p.copy(s, t, options);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, source);
    +        }
         }
     
         @Override
    @@ -319,29 +352,45 @@ public void move(Path source, Path target, CopyOption... options) throws IOExcep
                 log.trace("move({})[{}]: {}[{}]", source, s, target, t);
             }
             FileSystemProvider p = provider(s);
    -        p.move(s, t, options);
    +        try {
    +            p.move(s, t, options);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, source);
    +        }
         }
     
         @Override
         public boolean isSameFile(Path path, Path path2) throws IOException {
             Path r = unroot(path);
             Path r2 = unroot(path2);
             FileSystemProvider p = provider(r);
    -        return p.isSameFile(r, r2);
    +        try {
    +            return p.isSameFile(r, r2);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public boolean isHidden(Path path) throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        return p.isHidden(r);
    +        try {
    +            return p.isHidden(r);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public FileStore getFileStore(Path path) throws IOException {
             RootedFileSystem fileSystem = getFileSystem(path);
             Path root = fileSystem.getRoot();
    -        return Files.getFileStore(root);
    +        try {
    +            return Files.getFileStore(root);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         protected RootedFileSystem getFileSystem(Path path) throws FileSystemNotFoundException {
    @@ -384,7 +433,11 @@ protected RootedFileSystem getFileSystem(Path path) throws FileSystemNotFoundExc
         public void checkAccess(Path path, AccessMode... modes) throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        p.checkAccess(r, modes);
    +        try {
    +            p.checkAccess(r, modes);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
    @@ -403,18 +456,26 @@ public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type
             }
     
             FileSystemProvider p = provider(r);
    -        return p.readAttributes(r, type, options);
    +        try {
    +            return p.readAttributes(r, type, options);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        Map<String, Object> attrs = p.readAttributes(r, attributes, options);
    -        if (log.isTraceEnabled()) {
    -            log.trace("readAttributes({})[{}] {}: {}", path, r, attributes, attrs);
    +        try {
    +            Map<String, Object> attrs = p.readAttributes(r, attributes, options);
    +            if (log.isTraceEnabled()) {
    +                log.trace("readAttributes({})[{}] {}: {}", path, r, attributes, attrs);
    +            }
    +            return attrs;
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
             }
    -        return attrs;
         }
     
         @Override
    @@ -424,7 +485,11 @@ public void setAttribute(Path path, String attribute, Object value, LinkOption..
                 log.trace("setAttribute({})[{}] {}={}", path, r, attribute, value);
             }
             FileSystemProvider p = provider(r);
    -        p.setAttribute(r, attribute, value, options);
    +        try {
    +            p.setAttribute(r, attribute, value, options);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         protected FileSystemProvider provider(Path path) {
    @@ -434,10 +499,33 @@ protected FileSystemProvider provider(Path path) {
     
         protected Path root(RootedFileSystem rfs, Path nat) {
             if (nat.isAbsolute()) {
    +            // preferred case - this isn't a symlink out of our jail
    +            if (nat.startsWith(rfs.getRoot())) {
    +                // If we have the same number of parts as the root, and start with the root, we must be the root.
    +                if (nat.getNameCount() == rfs.getRoot().getNameCount()) {
    +                    return rfs.getPath("/");
    +                }
    +
    +                // We are the root, and more. Get the first name past the root because of how getPath works
    +                String firstName = "/" + nat.getName(rfs.getRoot().getNameCount());
    +
    +                // the rooted path should have the number of parts past the root
    +                String[] varargs = new String[nat.getNameCount() - rfs.getRoot().getNameCount() - 1];
    +                int varargsCounter = 0;
    +                for (int i = 1 + rfs.getRoot().getNameCount(); i < nat.getNameCount(); i++) {
    +                    varargs[varargsCounter++] = nat.getName(i).toString();
    +                }
    +                return rfs.getPath(firstName, varargs);
    +            }
    +
    +            // This is the case where there's a symlink jailbreak, so we return a relative link as the directories above
    +            // the chroot don't make sense to present
    +            // The behavior with the fs class is that we follow the symlink. Note that this is dangerous.
                 Path root = rfs.getRoot();
                 Path rel = root.relativize(nat);
    -            return rfs.getPath("/" + rel.toString());
    +            return rfs.getPath("/" + rel);
             } else {
    +            // For a relative symlink, simply return it as a RootedPath. Note that this may break out of the chroot.
                 return rfs.getPath(nat.toString());
             }
         }
    @@ -466,39 +554,74 @@ protected Path unroot(Path path) {
          * @throws InvalidPathException If the resolved path is not a proper sub-path of the rooted file system
          */
         protected Path resolveLocalPath(RootedPath path) {
    -        RootedPath absPath = Objects.requireNonNull(path, "No rooted path to resolve").toAbsolutePath();
    -        RootedFileSystem rfs = absPath.getFileSystem();
    +        Objects.requireNonNull(path, "No rooted path to resolve");
    +        RootedFileSystem rfs = path.getFileSystem();
             Path root = rfs.getRoot();
    -        FileSystem lfs = root.getFileSystem();
    +        // initialize a list for the new file name parts
    +        Path resolved = IoUtils.chroot(root, path);
     
    -        String rSep = ValidateUtils.checkNotNullAndNotEmpty(rfs.getSeparator(), "No rooted file system separator");
    -        ValidateUtils.checkTrue(rSep.length() == 1, "Bad rooted file system separator: %s", rSep);
    -        char rootedSeparator = rSep.charAt(0);
    +        /*
    +         * This can happen for Windows since we represent its paths as /C:/some/path, so substring(1) yields
    +         * C:/some/path - which is resolved as an absolute path (which we don't want).
    +         *
    +         * This also is a security assertion to protect against unknown attempts to break out of the chroot jail
    +         */
    +        if (!resolved.normalize().startsWith(root)) {
    +            throw new InvalidPathException(root.toString(), "Not under root");
    +        }
    +        return resolved;
    +    }
     
    -        String lSep = ValidateUtils.checkNotNullAndNotEmpty(lfs.getSeparator(), "No local file system separator");
    -        ValidateUtils.checkTrue(lSep.length() == 1, "Bad local file system separator: %s", lSep);
    -        char localSeparator = lSep.charAt(0);
    +    private IOException translateIoException(IOException ex, Path rootedPath) {
    +        // cast is safe as path was unrooted earlier.
    +        RootedPath rootedPathCasted = (RootedPath) rootedPath;
    +        Path root = rootedPathCasted.getFileSystem().getRoot();
    +
    +        if (ex instanceof FileSystemException) {
    +            String file = fixExceptionFileName(root, rootedPath, ((FileSystemException) ex).getFile());
    +            String otherFile = fixExceptionFileName(root, rootedPath, ((FileSystemException) ex).getOtherFile());
    +            String reason = ((FileSystemException) ex).getReason();
    +            if (NoSuchFileException.class.equals(ex.getClass())) {
    +                return new NoSuchFileException(file, otherFile, reason);
    +            } else if (FileSystemLoopException.class.equals(ex.getClass())) {
    +                return new FileSystemLoopException(file);
    +            } else if (NotDirectoryException.class.equals(ex.getClass())) {
    +                return new NotDirectoryException(file);
    +            } else if (DirectoryNotEmptyException.class.equals(ex.getClass())) {
    +                return new DirectoryNotEmptyException(file);
    +            } else if (NotLinkException.class.equals(ex.getClass())) {
    +                return new NotLinkException(file);
    +            } else if (AtomicMoveNotSupportedException.class.equals(ex.getClass())) {
    +                return new AtomicMoveNotSupportedException(file, otherFile, reason);
    +            } else if (FileAlreadyExistsException.class.equals(ex.getClass())) {
    +                return new FileAlreadyExistsException(file, otherFile, reason);
    +            } else if (AccessDeniedException.class.equals(ex.getClass())) {
    +                return new AccessDeniedException(file, otherFile, reason);
    +            }
    +            return new FileSystemException(file, otherFile, reason);
    +        } else if (ex.getClass().equals(FileNotFoundException.class)) {
    +            return new FileNotFoundException(ex.getLocalizedMessage().replace(root.toString(), ""));
    +        }
    +        // not sure how to translate, so leave as is. Hopefully does not leak data
    +        return ex;
    +    }
     
    -        String r = absPath.toString();
    -        String subPath = r.substring(1);
    -        if (rootedSeparator != localSeparator) {
    -            subPath = subPath.replace(rootedSeparator, localSeparator);
    +    private String fixExceptionFileName(Path root, Path rootedPath, String fileName) {
    +        if (fileName == null) {
    +            return null;
             }
     
    -        Path resolved = root.resolve(subPath);
    -        resolved = resolved.normalize();
    -        resolved = resolved.toAbsolutePath();
    -        if (log.isTraceEnabled()) {
    -            log.trace("resolveLocalPath({}): {}", absPath, resolved);
    +        Path toFix = root.getFileSystem().getPath(fileName);
    +        if (toFix.getNameCount() == root.getNameCount()) {
    +            // return the root
    +            return rootedPath.getFileSystem().getSeparator();
             }
     
    -        /*
    -         * This can happen for Windows since we represent its paths as /C:/some/path, so substring(1) yields
    -         * C:/some/path - which is resolved as an absolute path (which we don't want).
    -         */
    -        if (!resolved.startsWith(root)) {
    -            throw new InvalidPathException(r, "Not under root");
    +        StringBuilder ret = new StringBuilder();
    +        for (int partNum = root.getNameCount(); partNum < toFix.getNameCount(); partNum++) {
    +            ret.append(rootedPath.getFileSystem().getSeparator());
    +            ret.append(toFix.getName(partNum++));
             }
    -        return resolved;
    +        return ret.toString();
         }
     }
    
  • sshd-common/src/main/java/org/apache/sshd/common/file/root/RootedFileSystemUtils.java+55 0 added
    @@ -0,0 +1,55 @@
    +/*
    + * 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.common.file.root;
    +
    +import java.nio.file.InvalidPathException;
    +import java.nio.file.Path;
    +
    +/**
    + * Utility functions for rooted file utils
    + */
    +public final class RootedFileSystemUtils {
    +
    +    private RootedFileSystemUtils() {
    +        // do not construct
    +    }
    +
    +    /**
    +     * Validate that the relative path target is safe. This means that at no point in the path can there be more ".."
    +     * than path parts.
    +     *
    +     * @param target the target directory to validate is safe.
    +     */
    +    public static void validateSafeRelativeSymlink(Path target) {
    +        int numNames = 0;
    +        int numCdUps = 0;
    +        for (int i = 0; i < target.getNameCount(); i++) {
    +            if ("..".equals(target.getName(i).toString())) {
    +                numCdUps++;
    +            } else if (!".".equals(target.getName(i).toString())) {
    +                numNames++;
    +            }
    +
    +            // need to check at each part to prevent data leakage outside of chroot
    +            if (numCdUps > numNames) {
    +                throw new InvalidPathException(target.toString(), "Symlink would exit chroot: " + target);
    +            }
    +        }
    +    }
    +}
    
  • sshd-common/src/main/java/org/apache/sshd/common/file/root/RootedSecureDirectoryStream.java+92 0 added
    @@ -0,0 +1,92 @@
    +/*
    + * 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.common.file.root;
    +
    +import java.io.IOException;
    +import java.nio.channels.SeekableByteChannel;
    +import java.nio.file.LinkOption;
    +import java.nio.file.OpenOption;
    +import java.nio.file.Path;
    +import java.nio.file.SecureDirectoryStream;
    +import java.nio.file.attribute.FileAttribute;
    +import java.nio.file.attribute.FileAttributeView;
    +import java.util.Set;
    +
    +/**
    + * A secure directory stream proxy for a {@link RootedFileSystem}
    + *
    + * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
    + */
    +public class RootedSecureDirectoryStream extends RootedDirectoryStream implements SecureDirectoryStream<Path> {
    +
    +    public RootedSecureDirectoryStream(RootedFileSystem rfs, SecureDirectoryStream<Path> delegate) {
    +        super(rfs, delegate);
    +    }
    +
    +    @Override
    +    public SecureDirectoryStream<Path> newDirectoryStream(Path path, LinkOption... options) throws IOException {
    +        return new RootedSecureDirectoryStream(rfs, delegate().newDirectoryStream(fixPath(path), options));
    +    }
    +
    +    protected Path fixPath(Path p) {
    +        if (p.isAbsolute()) {
    +            return rfs.provider().unroot(p);
    +        }
    +
    +        // convert to root fs path.
    +        // Note: this IS able to go below the root directory by design - a way to break out of chroot.
    +        // Be very cautious using this.
    +        return rfs.getRootFileSystem().getPath(p.toString());
    +    }
    +
    +    @Override
    +    public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
    +            throws IOException {
    +        return delegate().newByteChannel(fixPath(path), options, attrs);
    +    }
    +
    +    @Override
    +    public void deleteFile(Path path) throws IOException {
    +        delegate().deleteFile(fixPath(path));
    +    }
    +
    +    @Override
    +    public void deleteDirectory(Path path) throws IOException {
    +        delegate().deleteDirectory(fixPath(path));
    +    }
    +
    +    @Override
    +    public void move(Path srcpath, SecureDirectoryStream<Path> targetdir, Path targetpath) throws IOException {
    +        delegate().move(fixPath(srcpath), targetdir, targetpath);
    +    }
    +
    +    @Override
    +    public <V extends FileAttributeView> V getFileAttributeView(Class<V> type) {
    +        return delegate().getFileAttributeView(type);
    +    }
    +
    +    @Override
    +    public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
    +        return delegate().getFileAttributeView(path, type, options);
    +    }
    +
    +    private SecureDirectoryStream<Path> delegate() {
    +        return (SecureDirectoryStream<Path>) delegate;
    +    }
    +}
    
  • sshd-common/src/main/java/org/apache/sshd/common/file/util/BaseFileSystem.java+20 3 modified
    @@ -34,6 +34,7 @@
     import java.util.regex.Pattern;
     
     import org.apache.sshd.common.util.GenericUtils;
    +import org.apache.sshd.common.util.OsUtils;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
     
    @@ -79,16 +80,15 @@ public Iterable<FileStore> getFileStores() {
         public T getPath(String first, String... more) {
             StringBuilder sb = new StringBuilder();
             if (!GenericUtils.isEmpty(first)) {
    -            appendDedupSep(sb, first.replace('\\', '/')); // in case we are running on Windows
    +            appendDedupSep(sb, handleWindowsSeparator(first));
             }
     
             if (GenericUtils.length(more) > 0) {
                 for (String segment : more) {
                     if ((sb.length() > 0) && (sb.charAt(sb.length() - 1) != '/')) {
                         sb.append('/');
                     }
    -                // in case we are running on Windows
    -                appendDedupSep(sb, segment.replace('\\', '/'));
    +                appendDedupSep(sb, handleWindowsSeparator(segment));
                 }
             }
     
    @@ -121,6 +121,23 @@ protected void appendDedupSep(StringBuilder sb, CharSequence s) {
             }
         }
     
    +    /**
    +     * In case we are running on Windows, accept "\\" as a file separator. Ignore in *nix as "\\" is a valid filename
    +     * 
    +     * @param  name the name to fix the separator for if running on Windows
    +     * @return      the fixed name
    +     */
    +    protected String handleWindowsSeparator(String name) {
    +        if (hostFsHasWindowsSeparator()) {
    +            return name.replace('\\', '/');
    +        }
    +        return name;
    +    }
    +
    +    protected boolean hostFsHasWindowsSeparator() {
    +        return OsUtils.isWin32();
    +    }
    +
         @Override
         public PathMatcher getPathMatcher(String syntaxAndPattern) {
             int colonIndex = Objects.requireNonNull(syntaxAndPattern, "No argument").indexOf(':');
    
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/IoUtils.java+152 8 modified
    @@ -47,6 +47,7 @@
     import java.util.Collection;
     import java.util.Collections;
     import java.util.EnumSet;
    +import java.util.Iterator;
     import java.util.List;
     import java.util.Objects;
     import java.util.Set;
    @@ -394,18 +395,13 @@ public static String getFileOwner(Path path, LinkOption... options) throws IOExc
          *                 explained above
          */
         public static Boolean checkFileExists(Path path, LinkOption... options) {
    -        boolean followLinks = true;
    -        for (LinkOption opt : options) {
    -            if (opt == LinkOption.NOFOLLOW_LINKS) {
    -                followLinks = false;
    -                break;
    -            }
    -        }
    +        boolean followLinks = followLinks(options);
    +
             try {
                 if (followLinks) {
                     path.getFileSystem().provider().checkAccess(path);
                 } else {
    -                Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
    +                Files.readAttributes(path, BasicFileAttributes.class, options);
                 }
                 return Boolean.TRUE;
             } catch (NoSuchFileException e) {
    @@ -415,6 +411,63 @@ public static Boolean checkFileExists(Path path, LinkOption... options) {
             }
         }
     
    +    /**
    +     * Checks that a file exists with or without following any symlinks.
    +     *
    +     * @param  path                the path to check
    +     * @param  neverFollowSymlinks whether to follow symlinks
    +     * @return                     true if the file exists with the symlink semantics, false if it doesn't exist, null
    +     *                             if symlinks were found, or it is unknown if whether the file exists
    +     */
    +    public static Boolean checkFileExistsAnySymlinks(Path path, boolean neverFollowSymlinks) {
    +        try {
    +            if (!neverFollowSymlinks) {
    +                path.getFileSystem().provider().checkAccess(path);
    +            } else {
    +                // this is a bad fix because this leaves a nasty race condition - the directory may turn into a symlink
    +                // between this check and the call to open()
    +                for (int i = 1; i <= path.getNameCount(); i++) {
    +                    Path checkForSymLink = getFirstPartsOfPath(path, i);
    +                    BasicFileAttributes basicFileAttributes
    +                            = Files.readAttributes(checkForSymLink, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
    +                    if (basicFileAttributes.isSymbolicLink()) {
    +                        return false;
    +                    }
    +                }
    +            }
    +            return true;
    +        } catch (NoSuchFileException e) {
    +            return false;
    +        } catch (IOException e) {
    +            return null;
    +        }
    +    }
    +
    +    /**
    +     * Extracts the first n parts of the path. For example <br>
    +     * ("/home/test/test12", 1) returns "/home", <br>
    +     * ("/home/test", 1) returns "/home/test" <br>
    +     * etc.
    +     *
    +     * @param  path           the path to extract parts of
    +     * @param  partsToExtract the number of parts to extract
    +     * @return                the extracted path
    +     */
    +    public static Path getFirstPartsOfPath(Path path, int partsToExtract) {
    +        String firstName = path.getName(0).toString();
    +        String[] names = new String[partsToExtract - 1];
    +        for (int j = 1; j < partsToExtract; j++) {
    +            names[j - 1] = path.getName(j).toString();
    +        }
    +        Path checkForSymLink = path.getFileSystem().getPath(firstName, names);
    +        // the root is not counted as a directory part so we must resolve the result relative to it.
    +        Path root = path.getRoot();
    +        if (root != null) {
    +            checkForSymLink = root.resolve(checkForSymLink);
    +        }
    +        return checkForSymLink;
    +    }
    +
         /**
          * Read the requested number of bytes or fail if there are not enough left.
          *
    @@ -637,4 +690,95 @@ public static List<String> readAllLines(BufferedReader reader, int lineCountHint
             }
             return result;
         }
    +
    +    /**
    +     * Chroot a path under the new root
    +     *
    +     * @param  newRoot    the new root
    +     * @param  toSanitize the path to sanitize and chroot
    +     * @return            the chrooted path under the newRoot filesystem
    +     */
    +    public static Path chroot(Path newRoot, Path toSanitize) {
    +        Objects.requireNonNull(newRoot);
    +        Objects.requireNonNull(toSanitize);
    +        List<String> sanitized = removeExtraCdUps(toSanitize);
    +        return buildPath(newRoot, newRoot.getFileSystem(), sanitized);
    +    }
    +
    +    /**
    +     * Remove any extra directory ups from the Path
    +     *
    +     * @param  toSanitize the path to sanitize
    +     * @return            the sanitized path
    +     */
    +    public static Path removeCdUpAboveRoot(Path toSanitize) {
    +        List<String> sanitized = removeExtraCdUps(toSanitize);
    +        return buildPath(toSanitize.getRoot(), toSanitize.getFileSystem(), sanitized);
    +    }
    +
    +    private static List<String> removeExtraCdUps(Path toResolve) {
    +        List<String> newNames = new ArrayList<>(toResolve.getNameCount());
    +
    +        int numCdUps = 0;
    +        int numDirParts = 0;
    +        for (int i = 0; i < toResolve.getNameCount(); i++) {
    +            String name = toResolve.getName(i).toString();
    +            if ("..".equals(name)) {
    +                // If we have more cdups than dir parts, so we ignore the ".." to avoid jail escapes
    +                if (numDirParts > numCdUps) {
    +                    ++numCdUps;
    +                    newNames.add(name);
    +                }
    +            } else {
    +                // if the current directory is a part of the name, don't increment number of dir parts, as it doesn't
    +                // add to the number of ".."s that can be present before the root
    +                if (!".".equals(name)) {
    +                    ++numDirParts;
    +                }
    +                newNames.add(name);
    +            }
    +        }
    +        return newNames;
    +    }
    +
    +    /**
    +     * Build a path from the list of path parts
    +     * 
    +     * @param  root      the root path
    +     * @param  fs        the filesystem
    +     * @param  namesList the parts of the path to build
    +     * @return           the built path
    +     */
    +    public static Path buildPath(Path root, FileSystem fs, List<String> namesList) {
    +        Objects.requireNonNull(fs);
    +        if (namesList == null) {
    +            return null;
    +        }
    +
    +        if (GenericUtils.isEmpty(namesList)) {
    +            return root == null ? fs.getPath(".") : root;
    +        }
    +
    +        Path cleanedPathToResolve = buildRelativePath(fs, namesList);
    +        return root == null ? cleanedPathToResolve : root.resolve(cleanedPathToResolve);
    +    }
    +
    +    /**
    +     * Build a relative path on the filesystem fs from the path parts in the namesList
    +     *
    +     * @param  fs        the filesystem for the path
    +     * @param  namesList the names list
    +     * @return           the built path
    +     */
    +    public static Path buildRelativePath(FileSystem fs, List<String> namesList) {
    +        String[] names = new String[namesList.size() - 1];
    +
    +        Iterator<String> it = namesList.iterator();
    +        String rootName = it.next();
    +        for (int i = 0; it.hasNext(); i++) {
    +            names[i] = it.next();
    +        }
    +        Path cleanedPathToResolve = fs.getPath(rootName, names);
    +        return cleanedPathToResolve;
    +    }
     }
    
  • sshd-common/src/test/java/org/apache/sshd/common/file/root/RootedFileSystemProviderTest.java+364 105 modified
    @@ -19,28 +19,29 @@
     
     package org.apache.sshd.common.file.root;
     
    +import java.io.File;
     import java.io.IOException;
    +import java.io.InputStream;
    +import java.io.OutputStream;
     import java.nio.ByteBuffer;
     import java.nio.channels.Channel;
     import java.nio.channels.FileChannel;
    -import java.nio.file.DirectoryStream;
    -import java.nio.file.FileSystem;
    -import java.nio.file.Files;
    -import java.nio.file.InvalidPathException;
    -import java.nio.file.OpenOption;
    -import java.nio.file.Path;
    -import java.nio.file.StandardCopyOption;
    -import java.nio.file.StandardOpenOption;
    +import java.nio.charset.StandardCharsets;
    +import java.nio.file.*;
    +import java.util.ArrayList;
     import java.util.Arrays;
     import java.util.Collections;
    +import java.util.List;
     import java.util.Objects;
     import java.util.Random;
     import java.util.TreeSet;
     
     import org.apache.sshd.common.util.GenericUtils;
    +import org.apache.sshd.common.util.OsUtils;
     import org.apache.sshd.util.test.CommonTestSupportUtils;
     import org.apache.sshd.util.test.NoIoTestCase;
    -import org.junit.BeforeClass;
    +import org.junit.Assert;
    +import org.junit.Assume;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
     import org.junit.experimental.categories.Category;
    @@ -49,7 +50,7 @@
     /**
      * Tests the RootedFileSystemProvider implementation of {@link java.nio.file.spi.FileSystemProvider} checking that
      * permissions for generic FS commands are not permitted outside of the root directory.
    - *
    + * <p>
      * Individual tests are form pairs (e.g. testX, testXInvalid) where testXInvalid is expected to test a parent path of
      * {@link RootedFileSystem#getRoot()}
      *
    @@ -58,19 +59,21 @@
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
     public class RootedFileSystemProviderTest extends AssertableFile {
    -    private static RootedFileSystem fileSystem;
    -    private static Path rootSandbox;
    +    private static final String SKIP_ON_WINDOWS = "Test fails due to windows normalizing paths before opening them, " +
    +                                                  "allowing one to open a file like \"C:\\directory_doesnt_exist\\..\\myfile.txt\" whereas this is blocked in unix";
    +    private static final String DOESNT_EXIST = "../doesnt_exist/../";
     
    -    public RootedFileSystemProviderTest() {
    -        super();
    -    }
    +    private final RootedFileSystem fileSystem;
    +    private final Path rootSandbox;
    +    private final FileHelper fileHelper;
     
    -    @BeforeClass
    -    public static void initializeFileSystem() throws IOException {
    +    public RootedFileSystemProviderTest() throws Exception {
    +        super();
    +        fileHelper = new FileHelper();
             Path targetFolder = Objects.requireNonNull(
                     CommonTestSupportUtils.detectTargetFolder(RootedFileSystemProviderTest.class),
                     "Failed to detect target folder");
    -        rootSandbox = FileHelper.createTestSandbox(targetFolder.resolve(TEMP_SUBFOLDER_NAME));
    +        rootSandbox = fileHelper.createTestSandbox(targetFolder.resolve(TEMP_SUBFOLDER_NAME));
             fileSystem = (RootedFileSystem) new RootedFileSystemProvider().newFileSystem(rootSandbox, Collections.emptyMap());
         }
     
    @@ -86,144 +89,216 @@ public void testRoot() {
         /* mkdir */
         @Test
         public void testMkdir() throws IOException {
    -        Path created = FileHelper.createDirectory(fileSystem.getPath(getCurrentTestName()));
    -        assertTrue(exists(created) && isDir(created) && isReadable(created));
    +        Path created = fileHelper.createDirectory(fileSystem.getPath(getCurrentTestName()));
    +        try {
    +            assertTrue(exists(created) && isDir(created) && isReadable(created));
    +        } finally {
    +            Files.delete(created);
    +        }
         }
     
    -    @Test(expected = InvalidPathException.class)
    -    public void testMkdirInvalid() throws IOException {
    -        Path parent = FileHelper.createDirectory(fileSystem.getPath("../" + getCurrentTestName()));
    -        fail(String.format("Unexpected success in creating directory %s", parent.toString()));
    +    @Test
    +    public void testMkdirInvalid() {
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +
    +        String parent = DOESNT_EXIST + getCurrentTestName();
    +        assertThrows(String.format("Unexpected success in creating directory %s", parent), NoSuchFileException.class,
    +                () -> fileHelper.createDirectory(fileSystem.getPath(parent)));
         }
     
         /* rmdir */
         @Test
         public void testRmdir() throws IOException {
    -        Path created = FileHelper.createDirectory(fileSystem.getPath(getCurrentTestName()));
    -        Path deleted = FileHelper.deleteDirectory(created);
    +        Path created = fileHelper.createDirectory(fileSystem.getPath(getCurrentTestName()));
    +        Path deleted = fileHelper.deleteDirectory(created);
             notExists(deleted);
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    @Test(expected = NoSuchFileException.class)
         public void testRmdirInvalid() throws IOException {
    -        Path deleted = FileHelper.deleteDirectory(fileSystem.getPath("../" + getCurrentTestName()));
    +        Path deleted = fileHelper.deleteDirectory(fileSystem.getPath(DOESNT_EXIST + getCurrentTestName()));
             fail(String.format("Unexpected success in removing directory %s", deleted.toString()));
         }
     
         /* chdir */
         @Test
         public void testChdir() throws IOException {
    -        Path created = FileHelper.createDirectory(fileSystem.getPath(getCurrentTestName()));
    -        Path createdFile = FileHelper.createFile(created.resolve(getCurrentTestName()));
    -        boolean hasFile = false;
    -        try (DirectoryStream<Path> ds = FileHelper.readDirectory(created)) {
    -            for (Path p : ds) {
    -                hasFile |= FileHelper.isSameFile(createdFile,
    -                        fileSystem.getPath(created.getFileName() + "/" + p.getFileName()));
    +        Path created = fileHelper.createDirectory(fileSystem.getPath(getCurrentTestName()));
    +        Path createdFile = fileHelper.createFile(created.resolve(getCurrentTestName()));
    +        try {
    +            boolean hasFile = false;
    +            try (DirectoryStream<Path> ds = fileHelper.readDirectory(created)) {
    +                for (Path p : ds) {
    +                    hasFile |= fileHelper.isSameFile(createdFile,
    +                            fileSystem.getPath(created.getFileName() + "/" + p.getFileName()));
    +                }
                 }
    +            assertTrue(createdFile + " found in ch directory", hasFile);
    +        } finally {
    +            Files.delete(createdFile);
    +            Files.delete(created);
             }
    -        assertTrue(createdFile + " found in ch directory", hasFile);
    -    }
    -
    -    @Test(expected = InvalidPathException.class)
    -    public void testChdirInvalid() throws IOException {
    -        Path chdir = FileHelper.createDirectory(fileSystem.getPath("../" + getCurrentTestName()));
    -        fail(String.format("Unexpected success in changing directory %s", chdir.toString()));
         }
     
         /* write */
         @Test
         public void testWriteFile() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
             assertTrue(exists(created) && isReadable(created));
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    @Test
         public void testWriteFileInvalid() throws IOException {
    -        Path written = FileHelper.createFile(fileSystem.getPath("../" + getCurrentTestName()));
    -        fail(String.format("Unexpected success in writing file %s", written.toString()));
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +
    +        String written = DOESNT_EXIST + getCurrentTestName();
    +        assertThrows(String.format("Unexpected success in writing file %s", written), NoSuchFileException.class,
    +                () -> fileHelper.createFile(fileSystem.getPath(written)));
         }
     
         /* read */
         @Test
         public void testReadFile() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    -        isNonEmpty(FileHelper.readFile(created));
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        isNonEmpty(fileHelper.readFile(created));
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    @Test(expected = NoSuchFileException.class)
         public void testReadFileInvalid() throws IOException {
    -        Path read = fileSystem.getPath("../" + getCurrentTestName());
    -        FileHelper.readFile(read);
    +        Path read = fileSystem.getPath(DOESNT_EXIST + getCurrentTestName());
    +        fileHelper.readFile(read);
             fail(String.format("Unexpected success in reading file %s", read.toString()));
         }
     
         /* rm */
         @Test
         public void testDeleteFile() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    -        Path deleted = FileHelper.deleteFile(created);
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        Path deleted = fileHelper.deleteFile(created);
             notExists(deleted);
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    @Test(expected = NoSuchFileException.class)
         public void testDeleteFileInvalid() throws IOException {
    -        Path deleted = FileHelper.deleteFile(fileSystem.getPath("../" + getCurrentTestName()));
    +        Path deleted = fileHelper.deleteFile(fileSystem.getPath(DOESNT_EXIST + getCurrentTestName()));
             fail(String.format("Unexpected success in deleting file %s", deleted.toString()));
         }
     
         /* cp */
         @Test
         public void testCopyFile() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
             Path destination = fileSystem.getPath(getCurrentTestName() + "dest");
    -        FileHelper.copyFile(created, destination);
    -        assertTrue(exists(destination) && isReadable(destination));
    +        try {
    +            fileHelper.copyFile(created, destination);
    +            assertTrue(exists(destination) && isReadable(destination));
    +        } finally {
    +            Files.delete(destination);
    +            Files.delete(created);
    +        }
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    @Test
         public void testCopyFileInvalid() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    -        Path copy = FileHelper.copyFile(created, fileSystem.getPath("../" + getCurrentTestName()));
    -        fail(String.format("Unexpected success in copying file to %s", copy.toString()));
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        String copy = DOESNT_EXIST + getCurrentTestName();
    +        assertThrows(String.format("Unexpected success in copying file to %s", copy),
    +                NoSuchFileException.class,
    +                () -> fileHelper.copyFile(created, fileSystem.getPath(copy)));
         }
     
         /* mv */
         @Test
         public void testMoveFile() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
             Path destination = fileSystem.getPath(getCurrentTestName() + "dest");
    -        FileHelper.moveFile(created, destination);
    +        fileHelper.moveFile(created, destination);
             assertTrue(notExists(created) && exists(destination) && isReadable(destination));
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    @Test
         public void testMoveFileInvalid() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    -        Path moved = FileHelper.moveFile(created, fileSystem.getPath("../" + getCurrentTestName()));
    -        fail(String.format("Unexpected success in moving file to %s", moved.toString()));
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        String moved = DOESNT_EXIST + getCurrentTestName();
    +        assertThrows(String.format("Unexpected success in moving file to %s", moved), NoSuchFileException.class,
    +                () -> fileHelper.moveFile(created, fileSystem.getPath(moved)));
         }
     
         /* link */
         @Test
         public void testCreateLink() throws IOException {
    -        Path existing = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        Path existing = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
             Path link = fileSystem.getPath(getCurrentTestName() + "link");
    -        FileHelper.createLink(link, existing);
    -        assertTrue(exists(link) && isReadable(link));
    +        try {
    +            fileHelper.createLink(link, existing);
    +            assertTrue(exists(link) && isReadable(link));
    +        } finally {
    +            Files.delete(link);
    +            Files.delete(existing);
    +        }
    +    }
    +
    +    @Test
    +    public void testJailbreakLink() throws IOException {
    +        testJailbreakLink("../");
    +    }
    +
    +    @Test
    +    public void testJailbreakLink2() throws IOException {
    +        testJailbreakLink("../test/");
    +    }
    +
    +    @Test
    +    public void testJailbreakLink3() throws IOException {
    +        testJailbreakLink("/..");
    +    }
    +
    +    @Test
    +    public void testJailbreakLink4() throws IOException {
    +        testJailbreakLink("/./..");
    +    }
    +
    +    @Test
    +    public void testJailbreakLink5() throws IOException {
    +        testJailbreakLink("/./../");
    +    }
    +
    +    @Test
    +    public void testJailbreakLink6() throws IOException {
    +        testJailbreakLink("./../");
    +    }
    +
    +    @Test
    +    public void testJailbreakLink7() throws IOException {
    +        String fileName = "/testdir/testdir2/../../..";
    +        testJailbreakLink(fileName);
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    private void testJailbreakLink(String jailbrokenTarget) throws IOException {
    +        Path target = fileSystem.getPath(jailbrokenTarget);
    +        Path linkPath = fileSystem.getPath("/testLink");
    +        Assert.assertThrows(InvalidPathException.class, () -> fileSystem.provider().createSymbolicLink(linkPath, target));
    +        Assert.assertFalse(Files.exists(linkPath));
    +    }
    +
    +    @Test
         public void testCreateLinkInvalid() throws IOException {
    -        Path existing = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    -        Path link = FileHelper.createLink(fileSystem.getPath("../" + getCurrentTestName() + "link"), existing);
    -        fail(String.format("Unexpected success in linking file %s", link.toString()));
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +
    +        Path existing = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        String link = DOESNT_EXIST + getCurrentTestName() + "link";
    +        assertThrows(String.format("Unexpected success in linking file %s", link), NoSuchFileException.class,
    +                () -> fileHelper.createLink(fileSystem.getPath(link), existing));
         }
     
         @Test
         public void testNewByteChannelProviderMismatchException() throws IOException {
             RootedFileSystemProvider provider = new RootedFileSystemProvider();
    -        Path tempFolder = assertHierarchyTargetFolderExists(getTempTargetFolder());
    +        Path tempFolder = getTempTargetFolder();
             Path file = Files.createTempFile(tempFolder, getCurrentTestName(), ".txt");
             try (FileSystem fs = provider.newFileSystem(tempFolder, Collections.emptyMap());
                  Channel channel = provider.newByteChannel(fs.getPath(file.getFileName().toString()), Collections.emptySet())) {
    @@ -235,23 +310,212 @@ public void testNewByteChannelProviderMismatchException() throws IOException {
         public void testResolveRoot() throws IOException {
             Path root = GenericUtils.head(fileSystem.getRootDirectories());
             Path dir = root.resolve("tsd");
    -        FileHelper.createDirectory(dir);
    -        Path f1 = FileHelper.createFile(dir.resolve("test.txt"));
    -        Path f2 = Files.newDirectoryStream(dir).iterator().next();
    -        assertTrue("Unrooted path found", f2 instanceof RootedPath);
    -        assertEquals(f1, f2);
    -        FileHelper.deleteFile(f1);
    -        FileHelper.deleteDirectory(dir);
    +        fileHelper.createDirectory(dir);
    +        Path f1 = fileHelper.createFile(dir.resolve("test.txt"));
    +        try {
    +            Path f2;
    +            try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
    +                f2 = ds.iterator().next();
    +            }
    +            assertTrue("Unrooted path found", f2 instanceof RootedPath);
    +            assertEquals(f1, f2);
    +        } finally {
    +            fileHelper.deleteFile(f1);
    +            fileHelper.deleteDirectory(dir);
    +        }
    +    }
    +
    +    @Test
    +    public void testBreakOutOfChroot1() throws IOException {
    +        String fileName = "../" + getCurrentTestName();
    +        testBreakOutOfChroot(fileName, fileName);
    +    }
    +
    +    @Test
    +    public void testBreakOutOfChroot2() throws IOException {
    +        String fileName = "./../" + getCurrentTestName();
    +        testBreakOutOfChroot(fileName, fileName);
    +    }
    +
    +    @Test
    +    public void testBreakOutOfChroot3() throws IOException {
    +        String fileName = "/../" + getCurrentTestName();
    +        testBreakOutOfChroot(fileName, fileName);
    +    }
    +
    +    @Test
    +    public void testBreakOutOfChroot4() throws IOException {
    +        String fileName = "/.././" + getCurrentTestName();
    +        testBreakOutOfChroot(fileName, fileName);
    +    }
    +
    +    @Test
    +    public void testBreakOutOfChroot5() throws IOException {
    +        String fileName = "/./../" + getCurrentTestName();
    +        testBreakOutOfChroot(fileName, fileName);
    +    }
    +
    +    @Test
    +    public void testBreakOutOfChroot6() throws IOException {
    +        String fileName = "//../" + getCurrentTestName();
    +        testBreakOutOfChroot(fileName, "/../" + getCurrentTestName());
    +    }
    +
    +    /**
    +     * Tests to make sure that the attempted break out of the chroot does not work with the specified filename
    +     *
    +     * @param  fileName    the filename to attempt to break out of the chroot with
    +     * @throws IOException on test failure
    +     */
    +    private void testBreakOutOfChroot(String fileName, String expected) throws IOException {
    +        RootedPath breakoutAttempt = fileSystem.getPath(fileName);
    +
    +        // make sure that our rooted fs behaves like a proper unix fs
    +        Assert.assertEquals(expected, breakoutAttempt.toString());
    +
    +        Path expectedDir = fileSystem.getRoot().resolve(getCurrentTestName());
    +        Path newDir = fileHelper.createDirectory(breakoutAttempt);
    +        try {
    +            assertTrue(Files.isDirectory(expectedDir));
    +
    +            String baseName = breakoutAttempt.getName(breakoutAttempt.getNameCount() - 1).toString();
    +            assertTrue(fileHelper.isSameFile(newDir, fileSystem.getPath(baseName)));
    +
    +            // make sure we didn't create it one directory out of the jail
    +            assertFalse(Files.exists(fileSystem.getRoot().resolve("../" + breakoutAttempt.getFileName().toString())));
    +
    +            // make sure various methods of referencing the file work.
    +            assertTrue(fileHelper.isSameFile(newDir, fileSystem.getPath("/" + baseName)));
    +            assertTrue(fileHelper.isSameFile(newDir, fileSystem.getPath("/../../" + baseName)));
    +            assertTrue(fileHelper.isSameFile(newDir, fileSystem.getPath("./../" + baseName)));
    +        } finally {
    +            // cleanup the directory.
    +            fileHelper.deleteDirectory(newDir);
    +        }
    +
    +        assertFalse(Files.isDirectory(expectedDir));
    +        assertFalse(Files.isDirectory(newDir));
    +    }
    +
    +    @Test
    +    public void testValidSymlink1() throws IOException {
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +        String fileName = "/testdir/../";
    +        testValidSymlink(fileName, true);
    +    }
    +
    +    @Test
    +    public void testValidSymlink2() throws IOException {
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +        String fileName = "/testdir/testdir2/../";
    +        testValidSymlink(fileName, true);
    +    }
    +
    +    @Test
    +    public void testValidSymlink3() throws IOException {
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +        String fileName = "/testdir/../testdir3/";
    +        testValidSymlink(fileName, true);
    +    }
    +
    +    @Test
    +    public void testValidSymlink4() throws IOException {
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +        String fileName = "testdir/../testdir3/../";
    +        testValidSymlink(fileName, true);
    +    }
    +
    +    @Test
    +    public void testValidSymlink5() throws IOException {
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +        String fileName = "testdir/../testdir3/../testfile";
    +        testValidSymlink(fileName, false);
    +    }
    +
    +    public void testValidSymlink(String symlink, boolean targetIsDirectory) throws IOException {
    +        Path target = fileSystem.getPath(symlink);
    +        Path linkPath = fileSystem.getPath("/testLink");
    +        final List<Path> toDelete = new ArrayList<>();
    +        try {
    +            fileSystem.provider().createSymbolicLink(linkPath, target);
    +            toDelete.add(linkPath);
    +
    +            // ensure that nothing processed the symlink.
    +            Assert.assertEquals(Paths.get(symlink).toString(),
    +                    fileSystem.provider().readSymbolicLink(linkPath).toString());
    +            Assert.assertFalse(Files.exists(target));
    +            Assert.assertEquals(Files.exists(linkPath), Files.exists(target));
    +
    +            // If we don't follow the link, we simply check that the link exists, which it does as we created it.
    +            Assert.assertTrue(Files.exists(linkPath, LinkOption.NOFOLLOW_LINKS));
    +
    +            createParentDirs(targetIsDirectory ? target : target.getParent(), toDelete);
    +
    +            if (!targetIsDirectory) {
    +                Files.createFile(target);
    +                toDelete.add(target);
    +            }
    +
    +            Assert.assertTrue(Files.exists(linkPath));
    +        } finally {
    +            for (int i = toDelete.size() - 1; i >= 0; i--) {
    +                Path path = toDelete.get(i);
    +                try {
    +                    Files.delete(path);
    +                } catch (IOException ex) {
    +                    // ignore as we might try to delete "/dir/.." which will fail as it contains dir..
    +                }
    +            }
    +        }
    +    }
    +
    +    private static void createParentDirs(Path target, List<Path> toDelete) throws IOException {
    +        if (target.getParent() != null) {
    +            createParentDirs(target.getParent(), toDelete);
    +        }
    +
    +        if (!Files.isDirectory(target)) {
    +            Files.createDirectories(target);
    +            toDelete.add(target);
    +        }
    +    }
    +
    +    @Test
    +    public void testFileNamedSlashOnUnixBasedOS() throws IOException {
    +        // skip ths test on Win32
    +        if (!"\\".equals(File.separator)) {
    +            Path slashFile = fileSystem.getPath("\\");
    +            Path created = fileHelper.createFile(slashFile);
    +            try {
    +                assertTrue(Files.isRegularFile(created));
    +            } finally {
    +                fileHelper.deleteFile(created);
    +            }
    +        }
    +    }
    +
    +    @Test
    +    public void testStreams() throws IOException {
    +        byte[] data = "This is test data".getBytes(StandardCharsets.UTF_8);
    +        RootedPath testPath = fileSystem.getPath("testfile.txt");
    +        try (OutputStream is = Files.newOutputStream(testPath)) {
    +            is.write(data);
    +        }
    +        byte[] read = new byte[data.length];
    +        try (InputStream is = Files.newInputStream(testPath)) {
    +            is.read(read);
    +        }
    +        assertArrayEquals(data, read);
         }
     
         /* Private helper */
     
         /**
          * Wrapper around the FileSystemProvider to test generic FS related commands. All created temp directories and files
    -     * used for testing are deleted upon JVM exit.
    +     * used for testing must be deleted in the test which creates them
          */
         @SuppressWarnings("synthetic-access")
    -    private static final class FileHelper {
    +    private final class FileHelper {
             private FileHelper() {
                 super();
             }
    @@ -263,71 +527,66 @@ private FileHelper() {
              * @return             the created sandbox Path
              * @throws IOException on failure to create
              */
    -        public static Path createTestSandbox(Path tempDir) throws IOException {
    -            Path created = Files.createDirectories(tempDir.resolve(RootedFileSystemProviderTest.class.getSimpleName()));
    -            created.toFile().deleteOnExit();
    +        public Path createTestSandbox(Path tempDir) throws IOException {
    +            Path path = tempDir.resolve(RootedFileSystemProviderTest.class.getSimpleName());
    +            Path created = Files.createDirectories(path);
                 return created;
             }
     
    -        public static Path createFile(Path source) throws InvalidPathException, IOException {
    +        public Path createFile(Path source) throws InvalidPathException, IOException {
                 try (FileChannel fc = fileSystem.provider().newFileChannel(source,
                         new TreeSet<OpenOption>(Arrays.asList(StandardOpenOption.CREATE, StandardOpenOption.WRITE)))) {
                     byte[] randomBytes = new byte[1000];
                     new Random().nextBytes(randomBytes);
                     fc.write(ByteBuffer.wrap(randomBytes));
    -                source.toFile().deleteOnExit();
                     return source;
                 }
             }
     
    -        public static Path createLink(Path link, Path existing) throws IOException {
    +        public Path createLink(Path link, Path existing) throws IOException {
                 fileSystem.provider().createLink(link, existing);
    -            link.toFile().deleteOnExit();
                 return link;
             }
     
    -        public static Path createDirectory(Path dir) throws InvalidPathException, IOException {
    +        public Path createDirectory(Path dir) throws InvalidPathException, IOException {
                 fileSystem.provider().createDirectory(dir);
    -            dir.toFile().deleteOnExit();
                 return dir;
             }
     
    -        public static Path deleteDirectory(Path dir) throws InvalidPathException, IOException {
    +        public Path deleteDirectory(Path dir) throws InvalidPathException, IOException {
                 return deleteFile(dir);
             }
     
    -        public static Path deleteFile(Path source) throws InvalidPathException, IOException {
    +        public Path deleteFile(Path source) throws InvalidPathException, IOException {
                 fileSystem.provider().delete(source);
                 return source;
             }
     
    -        public static byte[] readFile(Path source) throws IOException {
    +        public byte[] readFile(Path source) throws IOException {
                 try (FileChannel fc = fileSystem.provider().newFileChannel(source,
    -                    new TreeSet<OpenOption>(Arrays.asList(StandardOpenOption.READ)))) {
    -                byte[] readBytes = new byte[(int) source.toFile().length()];
    +                    Collections.singleton(StandardOpenOption.READ))) {
    +                byte[] readBytes = new byte[(int) Files.size(source)];
                     fc.read(ByteBuffer.wrap(readBytes));
                     return readBytes;
                 }
             }
     
    -        public static Path copyFile(Path source, Path destination) throws InvalidPathException, IOException {
    +        public Path copyFile(Path source, Path destination) throws InvalidPathException, IOException {
                 fileSystem.provider().copy(source, destination, StandardCopyOption.COPY_ATTRIBUTES);
    -            destination.toFile().deleteOnExit();
                 return destination;
             }
     
    -        public static Path moveFile(Path source, Path destination) throws InvalidPathException, IOException {
    +        public Path moveFile(Path source, Path destination) throws InvalidPathException, IOException {
                 fileSystem.provider().move(source, destination, StandardCopyOption.ATOMIC_MOVE);
    -            destination.toFile().deleteOnExit();
                 return destination;
             }
     
    -        public static DirectoryStream<Path> readDirectory(Path dir) throws InvalidPathException, IOException {
    +        public DirectoryStream<Path> readDirectory(Path dir) throws InvalidPathException, IOException {
                 DirectoryStream<Path> dirStream = fileSystem.provider().newDirectoryStream(dir, entry -> true);
                 return dirStream;
             }
     
    -        public static boolean isSameFile(Path source, Path destination) throws IOException {
    +        public boolean isSameFile(Path source, Path destination) throws IOException {
                 return fileSystem.provider().isSameFile(source, destination);
             }
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/io/IoUtilsTest.java+48 0 modified
    @@ -19,11 +19,17 @@
     
     package org.apache.sshd.common.util.io;
     
    +import java.io.IOException;
    +import java.nio.file.Files;
     import java.nio.file.LinkOption;
    +import java.nio.file.Path;
    +import java.nio.file.Paths;
     
     import org.apache.sshd.common.util.NumberUtils;
    +import org.apache.sshd.util.test.CommonTestSupportUtils;
     import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
    +import org.junit.Assert;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
     import org.junit.experimental.categories.Category;
    @@ -58,4 +64,46 @@ public void testGetEOLBytes() {
             }
         }
     
    +    /**
    +     * Tests to make sure check exists does not follow symlinks.
    +     *
    +     * @throws IOException on failure
    +     */
    +    @Test
    +    public void testCheckExists() throws IOException {
    +        testCheckExists(Paths.get("target/IoUtilsTest").toAbsolutePath());
    +    }
    +
    +    public void testCheckExists(Path baseDir) throws IOException {
    +        CommonTestSupportUtils.deleteRecursive(baseDir, LinkOption.NOFOLLOW_LINKS);
    +
    +        Path folder = baseDir.resolve("folder1/folder2/");
    +        Files.createDirectories(folder);
    +
    +        Path target = baseDir.resolve("folder1/target");
    +        Files.createDirectories(target);
    +
    +        Path dirInTarget = baseDir.resolve("folder1/target/dirintarget");
    +        Files.createDirectories(dirInTarget);
    +
    +        Files.createDirectories(target);
    +        Path link = baseDir.resolve("folder1/folder2/link");
    +        Files.createSymbolicLink(link, target);
    +
    +        Path link2 = baseDir.resolve("link");
    +        Files.createSymbolicLink(link2, target);
    +
    +        Path targetWithLink = baseDir.resolve("folder1/folder2/link/dirintarget");
    +
    +        Assert.assertTrue("symlink follow should work", IoUtils.checkFileExists(targetWithLink));
    +        Assert.assertTrue("symlink follow should work", IoUtils.checkFileExistsAnySymlinks(targetWithLink, false));
    +
    +        Assert.assertFalse("Link at end shouldn't be followed", IoUtils.checkFileExistsAnySymlinks(link, true));
    +        Assert.assertFalse("Nofollow shouldn't follow directory",
    +                IoUtils.checkFileExistsAnySymlinks(targetWithLink, true));
    +        Assert.assertFalse("Link at beginning shouldn't be followed",
    +                IoUtils.checkFileExistsAnySymlinks(link2, true));
    +        Assert.assertTrue("Root directory must exist",
    +                IoUtils.checkFileExistsAnySymlinks(baseDir, true));
    +    }
     }
    
  • sshd-common/src/test/java/org/apache/sshd/util/test/CommonTestSupportUtils.java+2 2 modified
    @@ -495,11 +495,11 @@ private static Path getFile(String resource) {
          * @throws IOException If failed to access/remove some file(s)
          */
         public static Path deleteRecursive(Path path, LinkOption... options) throws IOException {
    -        if ((path == null) || (!Files.exists(path))) {
    +        if ((path == null) || (!Files.exists(path, options))) {
                 return path;
             }
     
    -        if (Files.isDirectory(path)) {
    +        if (Files.isDirectory(path, options)) {
                 try (DirectoryStream<Path> ds = Files.newDirectoryStream(path)) {
                     for (Path child : ds) {
                         deleteRecursive(child, options);
    
  • sshd-sftp/src/main/java/org/apache/sshd/sftp/client/fs/SftpFileSystemProvider.java+2 2 modified
    @@ -645,7 +645,7 @@ public void copy(Path source, Path target, CopyOption... options) throws IOExcep
             }
     
             // delete target if it exists and REPLACE_EXISTING is specified
    -        Boolean status = IoUtils.checkFileExists(target, linkOptions);
    +        Boolean status = IoUtils.checkFileExistsAnySymlinks(target, noFollowLinks);
             if (status == null) {
                 throw new AccessDeniedException("Existence cannot be determined for copy target: " + target);
             }
    @@ -724,7 +724,7 @@ public void move(Path source, Path target, CopyOption... options) throws IOExcep
             }
     
             // delete target if it exists and REPLACE_EXISTING is specified
    -        Boolean status = IoUtils.checkFileExists(target, linkOptions);
    +        Boolean status = IoUtils.checkFileExistsAnySymlinks(target, noFollowLinks);
             if (status == null) {
                 throw new AccessDeniedException("Existence cannot be determined for move target " + target);
             }
    
  • sshd-sftp/src/main/java/org/apache/sshd/sftp/server/AbstractSftpSubsystemHelper.java+75 26 modified
    @@ -36,6 +36,7 @@
     import java.nio.file.LinkOption;
     import java.nio.file.NoSuchFileException;
     import java.nio.file.NotDirectoryException;
    +import java.nio.file.OpenOption;
     import java.nio.file.Path;
     import java.nio.file.StandardCopyOption;
     import java.nio.file.StandardOpenOption;
    @@ -51,7 +52,6 @@
     import java.util.Collection;
     import java.util.Collections;
     import java.util.Comparator;
    -import java.util.EnumSet;
     import java.util.HashSet;
     import java.util.LinkedHashMap;
     import java.util.LinkedList;
    @@ -645,9 +645,11 @@ protected Map<String, Object> doLStat(int id, String path, int flags) throws IOE
              * SSH_FXP_LSTAT does not.
              */
             SftpFileSystemAccessor accessor = getFileSystemAccessor();
    +
    +        boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_LSTAT, "", p);
             LinkOption[] options = accessor.resolveFileAccessLinkOptions(
                     this, p, SftpConstants.SSH_FXP_LSTAT, "", false);
    -        return resolveFileAttributes(p, flags, options);
    +        return resolveFileAttributes(p, flags, !followLinks, options);
         }
     
         protected void doSetStat(
    @@ -675,7 +677,7 @@ protected void doSetStat(
     
             Path p = resolveFile(path);
             if (followLinks == null) {
    -            followLinks = resolvePathResolutionFollowLinks(cmd, extension, p);
    +            followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_SETSTAT, extension, p);
             }
             doSetAttributes(cmd, extension, p, attrs, followLinks);
         }
    @@ -1408,12 +1410,14 @@ protected Map<String, Object> doStat(int id, String path, int flags) throws IOEx
              */
             Path p = resolveFile(path);
             SftpFileSystemAccessor accessor = getFileSystemAccessor();
    +        boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_STAT, "", p);
             LinkOption[] options = accessor.resolveFileAccessLinkOptions(
    -                this, p, SftpConstants.SSH_FXP_STAT, "", true);
    -        return resolveFileAttributes(p, flags, options);
    +                this, p, SftpConstants.SSH_FXP_STAT, "", followLinks);
    +        return resolveFileAttributes(p, flags, !followLinks, options);
         }
     
         protected void doRealPath(Buffer buffer, int id) throws IOException {
    +        // do things here.
             String path = buffer.getString();
             boolean debugEnabled = log.isDebugEnabled();
             ServerSession session = getServerSession();
    @@ -1467,7 +1471,6 @@ protected void doRealPath(Buffer buffer, int id) throws IOException {
                     result = doRealPathV6(id, path, extraPaths, p, options);
     
                     p = result.getKey();
    -                options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p);
                     Boolean status = result.getValue();
                     switch (control) {
                         case SftpConstants.SSH_FXP_REALPATH_STAT_IF:
    @@ -1557,14 +1560,14 @@ protected SimpleImmutableEntry<Path, Boolean> validateRealPath(
                 int id, String path, Path f, LinkOption... options)
                 throws IOException {
             Path p = normalize(f);
    -        Boolean status = IoUtils.checkFileExists(p, options);
    +        Boolean status = IoUtils.checkFileExistsAnySymlinks(p, !IoUtils.followLinks(options));
             return new SimpleImmutableEntry<>(p, status);
         }
     
         protected void doRemoveDirectory(Buffer buffer, int id) throws IOException {
             String path = buffer.getString();
             try {
    -            doRemoveDirectory(id, path, false);
    +            doRemoveDirectory(id, path);
             } catch (IOException | RuntimeException e) {
                 sendStatus(prepareReply(buffer), id, e,
                         SftpConstants.SSH_FXP_RMDIR, path);
    @@ -1574,15 +1577,23 @@ protected void doRemoveDirectory(Buffer buffer, int id) throws IOException {
             sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, "");
         }
     
    -    protected void doRemoveDirectory(int id, String path, boolean followLinks) throws IOException {
    +    protected void doRemoveDirectory(int id, String path) throws IOException {
             Path p = resolveFile(path);
             if (log.isDebugEnabled()) {
                 log.debug("doRemoveDirectory({})[id={}] SSH_FXP_RMDIR (path={})[{}]", getServerSession(), id, path, p);
             }
     
             SftpFileSystemAccessor accessor = getFileSystemAccessor();
    +
    +        final boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_RMDIR, "", p);
    +        Boolean symlinkCheck = validateParentExistWithNoSymlinksIfNeverFollowSymlinks(p, !followLinks);
    +        if (!Boolean.TRUE.equals(symlinkCheck)) {
    +            throw new AccessDeniedException(p.toString(), p.toString(),
    +                    "Parent directories do not exist ore are prohibited symlinks");
    +        }
    +
             LinkOption[] options = accessor.resolveFileAccessLinkOptions(
    -                this, p, SftpConstants.SSH_FXP_RMDIR, "", followLinks);
    +                this, p, SftpConstants.SSH_FXP_RMDIR, "", false);
             if (Files.isDirectory(p, options)) {
                 doRemove(id, p, true);
             } else {
    @@ -1618,7 +1629,7 @@ protected void doMakeDirectory(Buffer buffer, int id) throws IOException {
             String path = buffer.getString();
             Map<String, ?> attrs = readAttrs(buffer);
             try {
    -            doMakeDirectory(id, path, attrs, false);
    +            doMakeDirectory(id, path, attrs);
             } catch (IOException | RuntimeException e) {
                 sendStatus(prepareReply(buffer), id, e,
                         SftpConstants.SSH_FXP_MKDIR, path, attrs);
    @@ -1629,7 +1640,7 @@ protected void doMakeDirectory(Buffer buffer, int id) throws IOException {
         }
     
         protected void doMakeDirectory(
    -            int id, String path, Map<String, ?> attrs, boolean followLinks)
    +            int id, String path, Map<String, ?> attrs)
                 throws IOException {
             Path resolvedPath = resolveFile(path);
             ServerSession session = getServerSession();
    @@ -1640,14 +1651,21 @@ protected void doMakeDirectory(
     
             SftpFileSystemAccessor accessor = getFileSystemAccessor();
             LinkOption[] options = accessor.resolveFileAccessLinkOptions(
    -                this, resolvedPath, SftpConstants.SSH_FXP_MKDIR, "", followLinks);
    +                this, resolvedPath, SftpConstants.SSH_FXP_MKDIR, "", false);
    +        final boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_MKDIR, "", resolvedPath);
             SftpPathImpl.withAttributeCache(resolvedPath, p -> {
    -            Boolean status = IoUtils.checkFileExists(p, options);
    -            if (status == null) {
    +            Boolean symlinkCheck = validateParentExistWithNoSymlinksIfNeverFollowSymlinks(p, !followLinks);
    +            if (!Boolean.TRUE.equals(symlinkCheck)) {
    +                throw new AccessDeniedException(p.toString(), p.toString(),
    +                        "Parent directories do not exist ore are prohibited symlinks");
    +            }
    +
    +            Boolean fileExists = IoUtils.checkFileExists(p, options);
    +            if (fileExists == null) {
                     throw new AccessDeniedException(p.toString(), p.toString(), "Cannot validate make-directory existence");
                 }
     
    -            if (status) {
    +            if (fileExists) {
                     if (Files.isDirectory(p, options)) {
                         throw new FileAlreadyExistsException(p.toString(), p.toString(), "Target directory already exists");
                     } else {
    @@ -1661,7 +1679,6 @@ protected void doMakeDirectory(
             listener.creating(session, resolvedPath, attrs);
             try {
                 accessor.createDirectory(this, resolvedPath);
    -            followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_MKDIR, "", resolvedPath);
                 doSetAttributes(SftpConstants.SSH_FXP_MKDIR, "", resolvedPath, attrs, followLinks);
             } catch (IOException | RuntimeException | Error e) {
                 listener.created(session, resolvedPath, attrs, e);
    @@ -1676,7 +1693,7 @@ protected void doRemove(Buffer buffer, int id) throws IOException {
                 /*
                  * If 'filename' is a symbolic link, the link is removed, not the file it points to.
                  */
    -            doRemoveFile(id, path, false);
    +            doRemoveFile(id, path);
             } catch (IOException | RuntimeException e) {
                 sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_REMOVE, path);
                 return;
    @@ -1685,17 +1702,20 @@ protected void doRemove(Buffer buffer, int id) throws IOException {
             sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, "");
         }
     
    -    protected void doRemoveFile(int id, String path, boolean followLinks) throws IOException {
    +    protected void doRemoveFile(int id, String path) throws IOException {
             Path resolvedPath = resolveFile(path);
             if (log.isDebugEnabled()) {
                 log.debug("doRemoveFile({})[id={}] SSH_FXP_REMOVE (path={}[{}])", getServerSession(), id, path, resolvedPath);
             }
    +        // whether to follow links in the dir up to the final file
    +        boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_REMOVE, "", resolvedPath);
     
             SftpFileSystemAccessor accessor = getFileSystemAccessor();
    +        // never resolve links in the final path to remove as we want to remove the symlink, not the target
             LinkOption[] options = accessor.resolveFileAccessLinkOptions(
    -                this, resolvedPath, SftpConstants.SSH_FXP_REMOVE, "", followLinks);
    +                this, resolvedPath, SftpConstants.SSH_FXP_REMOVE, "", false);
             SftpPathImpl.withAttributeCache(resolvedPath, p -> {
    -            Boolean status = IoUtils.checkFileExists(p, options);
    +            Boolean status = checkSymlinkState(p, followLinks, options);
                 if (status == null) {
                     throw signalRemovalPreConditionFailure(id, path, p,
                             new AccessDeniedException(p.toString(), p.toString(), "Cannot determine existence of remove candidate"),
    @@ -2293,8 +2313,9 @@ protected void writeDirEntry(
                 int id, DirectoryHandle dir, Map<String, Path> entries, Buffer buffer,
                 int index, Path f, String shortName, LinkOption... options)
                 throws IOException {
    +        boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_READDIR, "", f);
             Map<String, ?> attrs = resolveFileAttributes(
    -                f, SftpConstants.SSH_FILEXFER_ATTR_ALL, options);
    +                f, SftpConstants.SSH_FILEXFER_ATTR_ALL, !followLinks, options);
             entries.put(shortName, f);
     
             SftpFileSystemAccessor accessor = getFileSystemAccessor();
    @@ -2392,10 +2413,10 @@ protected String getShortName(Path f) throws IOException {
         }
     
         protected NavigableMap<String, Object> resolveFileAttributes(
    -            Path path, int flags, LinkOption... options)
    +            Path path, int flags, boolean neverFollowSymLinks, LinkOption... options)
                 throws IOException {
             return SftpPathImpl.withAttributeCache(path, file -> {
    -            Boolean status = IoUtils.checkFileExists(file, options);
    +            Boolean status = checkSymlinkState(file, neverFollowSymLinks, options);
                 if (status == null) {
                     return handleUnknownStatusFileAttributes(file, flags, options);
                 } else if (!status) {
    @@ -2406,7 +2427,31 @@ protected NavigableMap<String, Object> resolveFileAttributes(
             });
         }
     
    -    protected void writeAttrs(Buffer buffer, Map<String, ?> attributes) throws IOException {
    +    /**
    +     * A utility function to validate that the directories leading up to a file are not symlinks
    +     *
    +     * @param  path                the file to check for symlink presence
    +     * @param  neverFollowSymLinks whether to never follow symlinks in the parent paths
    +     * @param  options             whether the file itself can be a symlink
    +     * @return                     whether there are symlinks in the path to this file, or null if unknown
    +     */
    +    public Boolean checkSymlinkState(Path path, boolean neverFollowSymLinks, LinkOption[] options) {
    +        Boolean status = validateParentExistWithNoSymlinksIfNeverFollowSymlinks(path, neverFollowSymLinks);
    +        if (!Boolean.FALSE.equals(status)) {
    +            status = IoUtils.checkFileExists(path, options);
    +        }
    +        return status;
    +    }
    +
    +    public Boolean validateParentExistWithNoSymlinksIfNeverFollowSymlinks(Path path, boolean neverFollowSymLinks) {
    +        Boolean status = true;
    +        if (neverFollowSymLinks && path.getParent() != null) {
    +            status = IoUtils.checkFileExistsAnySymlinks(path.getParent(), true);
    +        }
    +        return status;
    +    }
    +
    +    protected void writeAttrs(Buffer buffer, Map<String, ?> attributes) {
             SftpHelper.writeAttrs(buffer, getVersion(), attributes);
         }
     
    @@ -2653,7 +2698,11 @@ protected void setFileAttributes(
                     case IoUtils.SIZE_VIEW_ATTR: {
                         long newSize = ((Number) value).longValue();
                         SftpFileSystemAccessor accessor = getFileSystemAccessor();
    -                    Set<StandardOpenOption> openOptions = EnumSet.of(StandardOpenOption.WRITE);
    +                    Set<OpenOption> openOptions = new HashSet<>();
    +                    openOptions.add(StandardOpenOption.WRITE);
    +                    if (!IoUtils.followLinks(options)) {
    +                        openOptions.add(LinkOption.NOFOLLOW_LINKS);
    +                    }
                         try (SeekableByteChannel channel = accessor.openFile(this, null, file, null, openOptions)) {
                             channel.truncate(newSize);
                             accessor.closeFile(this, null, file, null, channel, openOptions);
    
  • sshd-sftp/src/main/java/org/apache/sshd/sftp/server/SftpFileSystemAccessor.java+83 11 modified
    @@ -19,20 +19,14 @@
     
     package org.apache.sshd.sftp.server;
     
    +import java.io.FileNotFoundException;
     import java.io.IOException;
     import java.io.StreamCorruptedException;
     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.*;
     import java.nio.file.attribute.AclEntry;
     import java.nio.file.attribute.AclFileAttributeView;
     import java.nio.file.attribute.FileAttribute;
    @@ -44,8 +38,10 @@
     import java.nio.file.attribute.UserPrincipalLookupService;
     import java.nio.file.attribute.UserPrincipalNotFoundException;
     import java.security.Principal;
    +import java.util.Arrays;
     import java.util.Collection;
     import java.util.Collections;
    +import java.util.HashSet;
     import java.util.List;
     import java.util.Map;
     import java.util.NavigableMap;
    @@ -214,7 +210,11 @@ default SeekableByteChannel openFile(
                 attrs = IoUtils.EMPTY_FILE_ATTRIBUTES;
             }
     
    -        return FileChannel.open(file, options, attrs);
    +        // Don't use Set contains as this can fail with TreeSet
    +        if (!Arrays.asList(options.toArray(new OpenOption[0])).contains(LinkOption.NOFOLLOW_LINKS)) {
    +            return FileChannel.open(file, options, attrs);
    +        }
    +        return seekableByteChannelNoLinkFollow(file, options, attrs);
         }
     
         /**
    @@ -317,9 +317,12 @@ default void closeFile(
          * @throws IOException If failed to open
          */
         default DirectoryStream<Path> openDirectory(
    -            SftpSubsystemProxy subsystem, DirectoryHandle dirHandle, Path dir, String handle)
    +            SftpSubsystemProxy subsystem, DirectoryHandle dirHandle, Path dir, String handle, LinkOption... linkOptions)
                 throws IOException {
    -        return Files.newDirectoryStream(dir);
    +        if (IoUtils.followLinks(linkOptions)) {
    +            return Files.newDirectoryStream(dir);
    +        }
    +        return secureResolveDirectoryStream(dir);
         }
     
         /**
    @@ -523,15 +526,84 @@ default void renameFile(
         default void copyFile(
                 SftpSubsystemProxy subsystem, Path src, Path dst, Collection<CopyOption> opts)
                 throws IOException {
    +
    +        if (noFollow(opts)) {
    +            try (SeekableByteChannel srcFile
    +                    = seekableByteChannelNoLinkFollow(src, Collections.singleton(StandardOpenOption.READ))) {
    +                if (!(srcFile instanceof FileChannel)) {
    +                    throw new UnsupportedOperationException("Host file system must return a file channel");
    +                }
    +
    +                try (SeekableByteChannel dstFile = seekableByteChannelNoLinkFollow(src, new HashSet<>(Arrays
    +                        .asList(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)))) {
    +                    ((FileChannel) srcFile).transferTo(0, srcFile.size(), dstFile);
    +                }
    +            }
    +            return;
    +        }
             Files.copy(src, dst,
                     GenericUtils.isEmpty(opts)
                             ? IoUtils.EMPTY_COPY_OPTIONS
                             : opts.toArray(new CopyOption[opts.size()]));
         }
     
    +    static SeekableByteChannel seekableByteChannelNoLinkFollow(
    +            Path src, Set<? extends OpenOption> opts, FileAttribute<?>... fileAttributes)
    +            throws IOException {
    +        if (src.getNameCount() < 1) {
    +            // opening root directory isn't supported.
    +            throw new IllegalArgumentException();
    +        }
    +        Path toResolve = src.isAbsolute() ? src : src.getFileSystem().getPath(src.toString());
    +
    +        if (!Files.isDirectory(src.getParent(), LinkOption.NOFOLLOW_LINKS)) {
    +            throw new FileNotFoundException(src.getParent().toString());
    +        }
    +
    +        toResolve = toResolve.normalize();
    +        try (SecureDirectoryStream<Path> ds = secureResolveDirectoryStream(toResolve.getParent())) {
    +            Set<OpenOption> newOpts = new HashSet<>(opts);
    +            newOpts.add(LinkOption.NOFOLLOW_LINKS);
    +            return ds.newByteChannel(toResolve.getName(toResolve.getNameCount() - 1), newOpts, fileAttributes);
    +        }
    +    }
    +
    +    static SecureDirectoryStream<Path> secureResolveDirectoryStream(Path toResolve) throws IOException {
    +        toResolve = IoUtils.removeCdUpAboveRoot(toResolve);
    +        DirectoryStream<Path> ds = Files.newDirectoryStream(toResolve.getRoot());
    +        for (int i = 0; i < toResolve.getNameCount(); i++) {
    +            DirectoryStream<Path> dsOld = ds;
    +            try {
    +                ds = secure(ds).newDirectoryStream(toResolve.getName(i), LinkOption.NOFOLLOW_LINKS);
    +                dsOld.close();
    +            } catch (IOException ex) {
    +                ds.close();
    +                throw ex;
    +            }
    +        }
    +        return secure(ds);
    +    }
    +
    +    static SecureDirectoryStream<Path> secure(DirectoryStream<Path> ds) {
    +        if (ds instanceof SecureDirectoryStream) {
    +            return (SecureDirectoryStream<Path>) ds;
    +        }
    +        // do we want to bomb? do we want a different fallback option?
    +        throw new UnsupportedOperationException("FS Does not support secure directory streams.");
    +    }
    +
         default void removeFile(
                 SftpSubsystemProxy subsystem, Path path, boolean isDirectory)
                 throws IOException {
             Files.delete(path);
         }
    +
    +    default boolean noFollow(Collection<?> opts) {
    +        for (Object opt : opts) {
    +            if (LinkOption.NOFOLLOW_LINKS.equals(opt)) {
    +                return true;
    +            }
    +        }
    +        return false;
    +    }
     }
    
  • sshd-sftp/src/main/java/org/apache/sshd/sftp/server/SftpSubsystem.java+5 3 modified
    @@ -687,7 +687,7 @@ protected void doReadDir(Buffer buffer, int id) throws IOException {
                 // the upstream server decide.
                 if (!(file instanceof SftpPath)) {
                     LinkOption[] options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_READDIR, "", file);
    -                Boolean status = IoUtils.checkFileExists(file, options);
    +                Boolean status = IoUtils.checkFileExistsAnySymlinks(file, !IoUtils.followLinks(options));
                     if (status == null) {
                         throw new AccessDeniedException(file.toString(), file.toString(), "Cannot determine existence of read-dir");
                     }
    @@ -748,7 +748,7 @@ protected void doReadDir(Buffer buffer, int id) throws IOException {
         @Override
         protected String doOpenDir(int id, String path, Path dir, LinkOption... options) throws IOException {
             SftpPathImpl.withAttributeCache(dir, p -> {
    -            Boolean status = IoUtils.checkFileExists(p, options);
    +            Boolean status = IoUtils.checkFileExistsAnySymlinks(p, !IoUtils.followLinks(options));
                 if (status == null) {
                     throw signalOpenFailure(id, path, p, true,
                             new AccessDeniedException(p.toString(), p.toString(), "Cannot determine open-dir existence"));
    @@ -808,7 +808,9 @@ protected Map<String, Object> doFStat(int id, String handle, int flags) throws I
             Path file = fileHandle.getFile();
             LinkOption[] options = accessor.resolveFileAccessLinkOptions(
                     this, file, SftpConstants.SSH_FXP_FSTAT, "", true);
    -        return resolveFileAttributes(file, flags, options);
    +
    +        boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_FSTAT, handle, file);
    +        return resolveFileAttributes(file, flags, followLinks, options);
         }
     
         @Override
    
  • sshd-sftp/src/test/java/org/apache/sshd/sftp/client/SftpTest.java+4 3 modified
    @@ -383,7 +383,7 @@ private void testCannotEscapeRoot(boolean useAbsolutePath) throws Exception {
                 SftpClient.Attributes attrs = sftp.stat(escapePath);
                 fail("Unexpected escape success for path=" + escapePath + ": " + attrs);
             } catch (SftpException e) {
    -            int expected = OsUtils.isWin32() || (!useAbsolutePath)
    +            int expected = OsUtils.isWin32() && useAbsolutePath
                         ? SftpConstants.SSH_FX_INVALID_FILENAME
                         : SftpConstants.SSH_FX_NO_SUCH_FILE;
                 assertEquals("Mismatched status for " + escapePath,
    @@ -711,10 +711,11 @@ public SeekableByteChannel openFile(
     
                     @Override
                     public DirectoryStream<Path> openDirectory(
    -                        SftpSubsystemProxy subsystem, DirectoryHandle dirHandle, Path dir, String handle)
    +                        SftpSubsystemProxy subsystem, DirectoryHandle dirHandle, Path dir, String handle,
    +                        LinkOption... linkOptions)
                             throws IOException {
                         dirHolder.set(dir);
    -                    return SftpFileSystemAccessor.super.openDirectory(subsystem, dirHandle, dir, handle);
    +                    return SftpFileSystemAccessor.super.openDirectory(subsystem, dirHandle, dir, handle, linkOptions);
                     }
     
                     @Override
    
  • sshd-sftp/src/test/java/org/apache/sshd/sftp/client/SftpVersionsTest.java+8 4 modified
    @@ -291,9 +291,11 @@ public void testSftpACLEncodeDecode() throws Exception {
                 public Command createSubsystem(ChannelSession channel) throws IOException {
                     SftpSubsystem subsystem = new SftpSubsystem(channel, this) {
                         @Override
    -                    protected NavigableMap<String, Object> resolveFileAttributes(Path file, int flags, LinkOption... options)
    +                    protected NavigableMap<String, Object> resolveFileAttributes(
    +                            Path file, int flags, boolean neverFollowSymLinks, LinkOption... options)
                                 throws IOException {
    -                        NavigableMap<String, Object> attrs = super.resolveFileAttributes(file, flags, options);
    +                        NavigableMap<String, Object> attrs
    +                                = super.resolveFileAttributes(file, flags, neverFollowSymLinks, options);
                             if (MapEntryUtils.isEmpty(attrs)) {
                                 attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
                             }
    @@ -412,9 +414,11 @@ public void testSftpExtensionsEncodeDecode() throws Exception {
                 public Command createSubsystem(ChannelSession channel) throws IOException {
                     SftpSubsystem subsystem = new SftpSubsystem(channel, this) {
                         @Override
    -                    protected NavigableMap<String, Object> resolveFileAttributes(Path file, int flags, LinkOption... options)
    +                    protected NavigableMap<String, Object> resolveFileAttributes(
    +                            Path file, int flags, boolean neverFollowLinks, LinkOption... options)
                                 throws IOException {
    -                        NavigableMap<String, Object> attrs = super.resolveFileAttributes(file, flags, options);
    +                        NavigableMap<String, Object> attrs
    +                                = super.resolveFileAttributes(file, flags, neverFollowLinks, options);
                             if (MapEntryUtils.isEmpty(attrs)) {
                                 attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
                             }
    
c20739b43aab

[SSHD-1234] Rooted file system can leak informations

https://github.com/apache/mina-sshdGuillaume NodetMay 9, 2023via ghsa
16 files changed · +1203 269
  • sshd-common/src/main/java/org/apache/sshd/common/file/root/RootedDirectoryStream.java+63 0 added
    @@ -0,0 +1,63 @@
    +/*
    + * 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.common.file.root;
    +
    +import java.io.IOException;
    +import java.nio.file.DirectoryStream;
    +import java.nio.file.Path;
    +import java.util.Iterator;
    +
    +/**
    + * secure directory stream proxy for a {@link RootedFileSystem}
    + *
    + * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
    + */
    +public class RootedDirectoryStream implements DirectoryStream<Path> {
    +    protected final RootedFileSystem rfs;
    +    protected final DirectoryStream<Path> delegate;
    +
    +    public RootedDirectoryStream(RootedFileSystem rfs, DirectoryStream<Path> delegate) {
    +        this.rfs = rfs;
    +        this.delegate = delegate;
    +    }
    +
    +    @Override
    +    public Iterator<Path> iterator() {
    +        return root(rfs, delegate.iterator());
    +    }
    +
    +    @Override
    +    public void close() throws IOException {
    +        delegate.close();
    +    }
    +
    +    protected Iterator<Path> root(RootedFileSystem rfs, Iterator<Path> iter) {
    +        return new Iterator<Path>() {
    +            @Override
    +            public boolean hasNext() {
    +                return iter.hasNext();
    +            }
    +
    +            @Override
    +            public Path next() {
    +                return rfs.provider().root(rfs, iter.next());
    +            }
    +        };
    +    }
    +}
    
  • sshd-common/src/main/java/org/apache/sshd/common/file/root/RootedFileSystem.java+5 0 modified
    @@ -94,6 +94,11 @@ public Iterable<FileStore> getFileStores() {
             return rootFs.getFileStores();
         }
     
    +    @Override
    +    protected boolean hostFsHasWindowsSeparator() {
    +        return "\\".equals(getRoot().getFileSystem().getSeparator());
    +    }
    +
         @Override
         public String toString() {
             return rootPath.toString();
    
  • sshd-common/src/main/java/org/apache/sshd/common/file/root/RootedFileSystemProvider.java+225 102 modified
    @@ -18,6 +18,7 @@
      */
     package org.apache.sshd.common.file.root;
     
    +import java.io.FileNotFoundException;
     import java.io.IOException;
     import java.io.InputStream;
     import java.io.OutputStream;
    @@ -26,32 +27,40 @@
     import java.nio.channels.AsynchronousFileChannel;
     import java.nio.channels.FileChannel;
     import java.nio.channels.SeekableByteChannel;
    +import java.nio.file.AccessDeniedException;
     import java.nio.file.AccessMode;
    +import java.nio.file.AtomicMoveNotSupportedException;
     import java.nio.file.CopyOption;
    +import java.nio.file.DirectoryNotEmptyException;
     import java.nio.file.DirectoryStream;
    +import java.nio.file.FileAlreadyExistsException;
     import java.nio.file.FileStore;
     import java.nio.file.FileSystem;
     import java.nio.file.FileSystemAlreadyExistsException;
    +import java.nio.file.FileSystemException;
    +import java.nio.file.FileSystemLoopException;
     import java.nio.file.FileSystemNotFoundException;
     import java.nio.file.Files;
     import java.nio.file.InvalidPathException;
     import java.nio.file.LinkOption;
    +import java.nio.file.NoSuchFileException;
    +import java.nio.file.NotDirectoryException;
    +import java.nio.file.NotLinkException;
     import java.nio.file.OpenOption;
     import java.nio.file.Path;
     import java.nio.file.Paths;
     import java.nio.file.ProviderMismatchException;
    +import java.nio.file.SecureDirectoryStream;
     import java.nio.file.attribute.BasicFileAttributes;
     import java.nio.file.attribute.FileAttribute;
     import java.nio.file.attribute.FileAttributeView;
     import java.nio.file.spi.FileSystemProvider;
     import java.util.HashMap;
    -import java.util.Iterator;
     import java.util.Map;
     import java.util.Objects;
     import java.util.Set;
     import java.util.concurrent.ExecutorService;
     
    -import org.apache.sshd.common.util.ValidateUtils;
     import org.apache.sshd.common.util.io.IoUtils;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
    @@ -157,22 +166,34 @@ public Path getPath(URI uri) {
         public InputStream newInputStream(Path path, OpenOption... options) throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        return p.newInputStream(r, options);
    +        try {
    +            return p.newInputStream(r, options);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public OutputStream newOutputStream(Path path, OpenOption... options) throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        return p.newOutputStream(r, options);
    +        try {
    +            return p.newOutputStream(r, options);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public FileChannel newFileChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
                 throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        return p.newFileChannel(r, options, attrs);
    +        try {
    +            return p.newFileChannel(r, options, attrs);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
    @@ -181,90 +202,86 @@ public AsynchronousFileChannel newAsynchronousFileChannel(
                 throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        return p.newAsynchronousFileChannel(r, options, executor, attrs);
    +        try {
    +            return p.newAsynchronousFileChannel(r, options, executor, attrs);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
                 throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        return p.newByteChannel(r, options, attrs);
    +        try {
    +            return p.newByteChannel(r, options, attrs);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
             Path r = unroot(dir);
             FileSystemProvider p = provider(r);
    -        return root(((RootedPath) dir).getFileSystem(), p.newDirectoryStream(r, filter));
    +        try {
    +            return root(((RootedPath) dir).getFileSystem(), p.newDirectoryStream(r, filter));
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, dir);
    +        }
         }
     
         protected DirectoryStream<Path> root(RootedFileSystem rfs, DirectoryStream<Path> ds) {
    -        return new DirectoryStream<Path>() {
    -            @Override
    -            public Iterator<Path> iterator() {
    -                return root(rfs, ds.iterator());
    -            }
    -
    -            @Override
    -            public void close() throws IOException {
    -                ds.close();
    -            }
    -        };
    -    }
    -
    -    protected Iterator<Path> root(RootedFileSystem rfs, Iterator<Path> iter) {
    -        return new Iterator<Path>() {
    -            @Override
    -            public boolean hasNext() {
    -                return iter.hasNext();
    -            }
    -
    -            @Override
    -            public Path next() {
    -                return root(rfs, iter.next());
    -            }
    -        };
    +        if (ds instanceof SecureDirectoryStream) {
    +            return new RootedSecureDirectoryStream(rfs, (SecureDirectoryStream<Path>) ds);
    +        }
    +        return new RootedDirectoryStream(rfs, ds);
         }
     
         @Override
         public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
             Path r = unroot(dir);
             FileSystemProvider p = provider(r);
    -        p.createDirectory(r, attrs);
    +        try {
    +            p.createDirectory(r, attrs);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, dir);
    +        }
         }
     
         @Override
         public void createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs) throws IOException {
    -        createLink(link, target, true, attrs);
    -    }
    -
    -    @Override
    -    public void createLink(Path link, Path existing) throws IOException {
    -        createLink(link, existing, false);
    -    }
    -
    -    protected void createLink(Path link, Path target, boolean symLink, FileAttribute<?>... attrs) throws IOException {
    +        // make sure symlink cannot break out of chroot jail. If it is unsafe, simply thrown an exception. This is
    +        // to ensure that symlink semantics are maintained when it is safe, and creation fails when not.
    +        RootedFileSystemUtils.validateSafeRelativeSymlink(target);
             Path l = unroot(link);
    -        Path t = unroot(target);
    -        /*
    -         * For a symbolic link preserve the relative path
    -         */
    -        if (symLink && (!target.isAbsolute())) {
    -            RootedFileSystem rfs = ((RootedPath) target).getFileSystem();
    -            Path root = rfs.getRoot();
    -            t = root.relativize(t);
    -        }
    +        Path t = target.isAbsolute() ? unroot(target) : l.getFileSystem().getPath(target.toString());
     
             FileSystemProvider p = provider(l);
    -        if (symLink) {
    +        try {
                 p.createSymbolicLink(l, t, attrs);
    -        } else {
    -            p.createLink(l, t);
    +
    +            if (log.isDebugEnabled()) {
    +                log.debug("createSymbolicLink({} => {}", l, t);
    +            }
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, link);
             }
    +    }
     
    -        if (log.isDebugEnabled()) {
    -            log.debug("createLink(symbolic={}) {} => {}", symLink, l, t);
    +    @Override
    +    public void createLink(Path link, Path existing) throws IOException {
    +        Path l = unroot(link);
    +        Path t = unroot(existing);
    +
    +        try {
    +            provider(l).createLink(l, t);
    +            if (log.isDebugEnabled()) {
    +                log.debug("createLink({} => {}", l, t);
    +            }
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, link);
             }
         }
     
    @@ -275,7 +292,11 @@ public void delete(Path path) throws IOException {
                 log.trace("delete({}): {}", path, r);
             }
             FileSystemProvider p = provider(r);
    -        p.delete(r);
    +        try {
    +            p.delete(r);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
    @@ -285,19 +306,27 @@ public boolean deleteIfExists(Path path) throws IOException {
                 log.trace("deleteIfExists({}): {}", path, r);
             }
             FileSystemProvider p = provider(r);
    -        return p.deleteIfExists(r);
    +        try {
    +            return p.deleteIfExists(r);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public Path readSymbolicLink(Path link) throws IOException {
             Path r = unroot(link);
             FileSystemProvider p = provider(r);
    -        Path t = p.readSymbolicLink(r);
    -        Path target = root((RootedFileSystem) link.getFileSystem(), t);
    -        if (log.isTraceEnabled()) {
    -            log.trace("readSymbolicLink({})[{}]: {}[{}]", link, r, target, t);
    +        try {
    +            Path t = p.readSymbolicLink(r);
    +            Path target = root((RootedFileSystem) link.getFileSystem(), t);
    +            if (log.isTraceEnabled()) {
    +                log.trace("readSymbolicLink({})[{}]: {}[{}]", link, r, target, t);
    +            }
    +            return target;
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, link);
             }
    -        return target;
         }
     
         @Override
    @@ -308,7 +337,11 @@ public void copy(Path source, Path target, CopyOption... options) throws IOExcep
                 log.trace("copy({})[{}]: {}[{}]", source, s, target, t);
             }
             FileSystemProvider p = provider(s);
    -        p.copy(s, t, options);
    +        try {
    +            p.copy(s, t, options);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, source);
    +        }
         }
     
         @Override
    @@ -319,29 +352,45 @@ public void move(Path source, Path target, CopyOption... options) throws IOExcep
                 log.trace("move({})[{}]: {}[{}]", source, s, target, t);
             }
             FileSystemProvider p = provider(s);
    -        p.move(s, t, options);
    +        try {
    +            p.move(s, t, options);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, source);
    +        }
         }
     
         @Override
         public boolean isSameFile(Path path, Path path2) throws IOException {
             Path r = unroot(path);
             Path r2 = unroot(path2);
             FileSystemProvider p = provider(r);
    -        return p.isSameFile(r, r2);
    +        try {
    +            return p.isSameFile(r, r2);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public boolean isHidden(Path path) throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        return p.isHidden(r);
    +        try {
    +            return p.isHidden(r);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public FileStore getFileStore(Path path) throws IOException {
             RootedFileSystem fileSystem = getFileSystem(path);
             Path root = fileSystem.getRoot();
    -        return Files.getFileStore(root);
    +        try {
    +            return Files.getFileStore(root);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         protected RootedFileSystem getFileSystem(Path path) throws FileSystemNotFoundException {
    @@ -384,7 +433,11 @@ protected RootedFileSystem getFileSystem(Path path) throws FileSystemNotFoundExc
         public void checkAccess(Path path, AccessMode... modes) throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        p.checkAccess(r, modes);
    +        try {
    +            p.checkAccess(r, modes);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
    @@ -403,18 +456,26 @@ public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type
             }
     
             FileSystemProvider p = provider(r);
    -        return p.readAttributes(r, type, options);
    +        try {
    +            return p.readAttributes(r, type, options);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         @Override
         public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
             Path r = unroot(path);
             FileSystemProvider p = provider(r);
    -        Map<String, Object> attrs = p.readAttributes(r, attributes, options);
    -        if (log.isTraceEnabled()) {
    -            log.trace("readAttributes({})[{}] {}: {}", path, r, attributes, attrs);
    +        try {
    +            Map<String, Object> attrs = p.readAttributes(r, attributes, options);
    +            if (log.isTraceEnabled()) {
    +                log.trace("readAttributes({})[{}] {}: {}", path, r, attributes, attrs);
    +            }
    +            return attrs;
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
             }
    -        return attrs;
         }
     
         @Override
    @@ -424,7 +485,11 @@ public void setAttribute(Path path, String attribute, Object value, LinkOption..
                 log.trace("setAttribute({})[{}] {}={}", path, r, attribute, value);
             }
             FileSystemProvider p = provider(r);
    -        p.setAttribute(r, attribute, value, options);
    +        try {
    +            p.setAttribute(r, attribute, value, options);
    +        } catch (IOException ex) {
    +            throw translateIoException(ex, path);
    +        }
         }
     
         protected FileSystemProvider provider(Path path) {
    @@ -434,10 +499,33 @@ protected FileSystemProvider provider(Path path) {
     
         protected Path root(RootedFileSystem rfs, Path nat) {
             if (nat.isAbsolute()) {
    +            // preferred case - this isn't a symlink out of our jail
    +            if (nat.startsWith(rfs.getRoot())) {
    +                // If we have the same number of parts as the root, and start with the root, we must be the root.
    +                if (nat.getNameCount() == rfs.getRoot().getNameCount()) {
    +                    return rfs.getPath("/");
    +                }
    +
    +                // We are the root, and more. Get the first name past the root because of how getPath works
    +                String firstName = "/" + nat.getName(rfs.getRoot().getNameCount());
    +
    +                // the rooted path should have the number of parts past the root
    +                String[] varargs = new String[nat.getNameCount() - rfs.getRoot().getNameCount() - 1];
    +                int varargsCounter = 0;
    +                for (int i = 1 + rfs.getRoot().getNameCount(); i < nat.getNameCount(); i++) {
    +                    varargs[varargsCounter++] = nat.getName(i).toString();
    +                }
    +                return rfs.getPath(firstName, varargs);
    +            }
    +
    +            // This is the case where there's a symlink jailbreak, so we return a relative link as the directories above
    +            // the chroot don't make sense to present
    +            // The behavior with the fs class is that we follow the symlink. Note that this is dangerous.
                 Path root = rfs.getRoot();
                 Path rel = root.relativize(nat);
    -            return rfs.getPath("/" + rel.toString());
    +            return rfs.getPath("/" + rel);
             } else {
    +            // For a relative symlink, simply return it as a RootedPath. Note that this may break out of the chroot.
                 return rfs.getPath(nat.toString());
             }
         }
    @@ -466,39 +554,74 @@ protected Path unroot(Path path) {
          * @throws InvalidPathException If the resolved path is not a proper sub-path of the rooted file system
          */
         protected Path resolveLocalPath(RootedPath path) {
    -        RootedPath absPath = Objects.requireNonNull(path, "No rooted path to resolve").toAbsolutePath();
    -        RootedFileSystem rfs = absPath.getFileSystem();
    +        Objects.requireNonNull(path, "No rooted path to resolve");
    +        RootedFileSystem rfs = path.getFileSystem();
             Path root = rfs.getRoot();
    -        FileSystem lfs = root.getFileSystem();
    +        // initialize a list for the new file name parts
    +        Path resolved = IoUtils.chroot(root, path);
     
    -        String rSep = ValidateUtils.checkNotNullAndNotEmpty(rfs.getSeparator(), "No rooted file system separator");
    -        ValidateUtils.checkTrue(rSep.length() == 1, "Bad rooted file system separator: %s", rSep);
    -        char rootedSeparator = rSep.charAt(0);
    +        /*
    +         * This can happen for Windows since we represent its paths as /C:/some/path, so substring(1) yields
    +         * C:/some/path - which is resolved as an absolute path (which we don't want).
    +         *
    +         * This also is a security assertion to protect against unknown attempts to break out of the chroot jail
    +         */
    +        if (!resolved.normalize().startsWith(root)) {
    +            throw new InvalidPathException(root.toString(), "Not under root");
    +        }
    +        return resolved;
    +    }
     
    -        String lSep = ValidateUtils.checkNotNullAndNotEmpty(lfs.getSeparator(), "No local file system separator");
    -        ValidateUtils.checkTrue(lSep.length() == 1, "Bad local file system separator: %s", lSep);
    -        char localSeparator = lSep.charAt(0);
    +    private IOException translateIoException(IOException ex, Path rootedPath) {
    +        // cast is safe as path was unrooted earlier.
    +        RootedPath rootedPathCasted = (RootedPath) rootedPath;
    +        Path root = rootedPathCasted.getFileSystem().getRoot();
    +
    +        if (ex instanceof FileSystemException) {
    +            String file = fixExceptionFileName(root, rootedPath, ((FileSystemException) ex).getFile());
    +            String otherFile = fixExceptionFileName(root, rootedPath, ((FileSystemException) ex).getOtherFile());
    +            String reason = ((FileSystemException) ex).getReason();
    +            if (NoSuchFileException.class.equals(ex.getClass())) {
    +                return new NoSuchFileException(file, otherFile, reason);
    +            } else if (FileSystemLoopException.class.equals(ex.getClass())) {
    +                return new FileSystemLoopException(file);
    +            } else if (NotDirectoryException.class.equals(ex.getClass())) {
    +                return new NotDirectoryException(file);
    +            } else if (DirectoryNotEmptyException.class.equals(ex.getClass())) {
    +                return new DirectoryNotEmptyException(file);
    +            } else if (NotLinkException.class.equals(ex.getClass())) {
    +                return new NotLinkException(file);
    +            } else if (AtomicMoveNotSupportedException.class.equals(ex.getClass())) {
    +                return new AtomicMoveNotSupportedException(file, otherFile, reason);
    +            } else if (FileAlreadyExistsException.class.equals(ex.getClass())) {
    +                return new FileAlreadyExistsException(file, otherFile, reason);
    +            } else if (AccessDeniedException.class.equals(ex.getClass())) {
    +                return new AccessDeniedException(file, otherFile, reason);
    +            }
    +            return new FileSystemException(file, otherFile, reason);
    +        } else if (ex.getClass().equals(FileNotFoundException.class)) {
    +            return new FileNotFoundException(ex.getLocalizedMessage().replace(root.toString(), ""));
    +        }
    +        // not sure how to translate, so leave as is. Hopefully does not leak data
    +        return ex;
    +    }
     
    -        String r = absPath.toString();
    -        String subPath = r.substring(1);
    -        if (rootedSeparator != localSeparator) {
    -            subPath = subPath.replace(rootedSeparator, localSeparator);
    +    private String fixExceptionFileName(Path root, Path rootedPath, String fileName) {
    +        if (fileName == null) {
    +            return null;
             }
     
    -        Path resolved = root.resolve(subPath);
    -        resolved = resolved.normalize();
    -        resolved = resolved.toAbsolutePath();
    -        if (log.isTraceEnabled()) {
    -            log.trace("resolveLocalPath({}): {}", absPath, resolved);
    +        Path toFix = root.getFileSystem().getPath(fileName);
    +        if (toFix.getNameCount() == root.getNameCount()) {
    +            // return the root
    +            return rootedPath.getFileSystem().getSeparator();
             }
     
    -        /*
    -         * This can happen for Windows since we represent its paths as /C:/some/path, so substring(1) yields
    -         * C:/some/path - which is resolved as an absolute path (which we don't want).
    -         */
    -        if (!resolved.startsWith(root)) {
    -            throw new InvalidPathException(r, "Not under root");
    +        StringBuilder ret = new StringBuilder();
    +        for (int partNum = root.getNameCount(); partNum < toFix.getNameCount(); partNum++) {
    +            ret.append(rootedPath.getFileSystem().getSeparator());
    +            ret.append(toFix.getName(partNum++));
             }
    -        return resolved;
    +        return ret.toString();
         }
     }
    
  • sshd-common/src/main/java/org/apache/sshd/common/file/root/RootedFileSystemUtils.java+55 0 added
    @@ -0,0 +1,55 @@
    +/*
    + * 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.common.file.root;
    +
    +import java.nio.file.InvalidPathException;
    +import java.nio.file.Path;
    +
    +/**
    + * Utility functions for rooted file utils
    + */
    +public final class RootedFileSystemUtils {
    +
    +    private RootedFileSystemUtils() {
    +        // do not construct
    +    }
    +
    +    /**
    +     * Validate that the relative path target is safe. This means that at no point in the path can there be more ".."
    +     * than path parts.
    +     *
    +     * @param target the target directory to validate is safe.
    +     */
    +    public static void validateSafeRelativeSymlink(Path target) {
    +        int numNames = 0;
    +        int numCdUps = 0;
    +        for (int i = 0; i < target.getNameCount(); i++) {
    +            if ("..".equals(target.getName(i).toString())) {
    +                numCdUps++;
    +            } else if (!".".equals(target.getName(i).toString())) {
    +                numNames++;
    +            }
    +
    +            // need to check at each part to prevent data leakage outside of chroot
    +            if (numCdUps > numNames) {
    +                throw new InvalidPathException(target.toString(), "Symlink would exit chroot: " + target);
    +            }
    +        }
    +    }
    +}
    
  • sshd-common/src/main/java/org/apache/sshd/common/file/root/RootedSecureDirectoryStream.java+92 0 added
    @@ -0,0 +1,92 @@
    +/*
    + * 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.common.file.root;
    +
    +import java.io.IOException;
    +import java.nio.channels.SeekableByteChannel;
    +import java.nio.file.LinkOption;
    +import java.nio.file.OpenOption;
    +import java.nio.file.Path;
    +import java.nio.file.SecureDirectoryStream;
    +import java.nio.file.attribute.FileAttribute;
    +import java.nio.file.attribute.FileAttributeView;
    +import java.util.Set;
    +
    +/**
    + * A secure directory stream proxy for a {@link RootedFileSystem}
    + *
    + * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
    + */
    +public class RootedSecureDirectoryStream extends RootedDirectoryStream implements SecureDirectoryStream<Path> {
    +
    +    public RootedSecureDirectoryStream(RootedFileSystem rfs, SecureDirectoryStream<Path> delegate) {
    +        super(rfs, delegate);
    +    }
    +
    +    @Override
    +    public SecureDirectoryStream<Path> newDirectoryStream(Path path, LinkOption... options) throws IOException {
    +        return new RootedSecureDirectoryStream(rfs, delegate().newDirectoryStream(fixPath(path), options));
    +    }
    +
    +    protected Path fixPath(Path p) {
    +        if (p.isAbsolute()) {
    +            return rfs.provider().unroot(p);
    +        }
    +
    +        // convert to root fs path.
    +        // Note: this IS able to go below the root directory by design - a way to break out of chroot.
    +        // Be very cautious using this.
    +        return rfs.getRootFileSystem().getPath(p.toString());
    +    }
    +
    +    @Override
    +    public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
    +            throws IOException {
    +        return delegate().newByteChannel(fixPath(path), options, attrs);
    +    }
    +
    +    @Override
    +    public void deleteFile(Path path) throws IOException {
    +        delegate().deleteFile(fixPath(path));
    +    }
    +
    +    @Override
    +    public void deleteDirectory(Path path) throws IOException {
    +        delegate().deleteDirectory(fixPath(path));
    +    }
    +
    +    @Override
    +    public void move(Path srcpath, SecureDirectoryStream<Path> targetdir, Path targetpath) throws IOException {
    +        delegate().move(fixPath(srcpath), targetdir, targetpath);
    +    }
    +
    +    @Override
    +    public <V extends FileAttributeView> V getFileAttributeView(Class<V> type) {
    +        return delegate().getFileAttributeView(type);
    +    }
    +
    +    @Override
    +    public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
    +        return delegate().getFileAttributeView(path, type, options);
    +    }
    +
    +    private SecureDirectoryStream<Path> delegate() {
    +        return (SecureDirectoryStream<Path>) delegate;
    +    }
    +}
    
  • sshd-common/src/main/java/org/apache/sshd/common/file/util/BaseFileSystem.java+20 3 modified
    @@ -34,6 +34,7 @@
     import java.util.regex.Pattern;
     
     import org.apache.sshd.common.util.GenericUtils;
    +import org.apache.sshd.common.util.OsUtils;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
     
    @@ -79,16 +80,15 @@ public Iterable<FileStore> getFileStores() {
         public T getPath(String first, String... more) {
             StringBuilder sb = new StringBuilder();
             if (!GenericUtils.isEmpty(first)) {
    -            appendDedupSep(sb, first.replace('\\', '/')); // in case we are running on Windows
    +            appendDedupSep(sb, handleWindowsSeparator(first));
             }
     
             if (GenericUtils.length(more) > 0) {
                 for (String segment : more) {
                     if ((sb.length() > 0) && (sb.charAt(sb.length() - 1) != '/')) {
                         sb.append('/');
                     }
    -                // in case we are running on Windows
    -                appendDedupSep(sb, segment.replace('\\', '/'));
    +                appendDedupSep(sb, handleWindowsSeparator(segment));
                 }
             }
     
    @@ -121,6 +121,23 @@ protected void appendDedupSep(StringBuilder sb, CharSequence s) {
             }
         }
     
    +    /**
    +     * In case we are running on Windows, accept "\\" as a file separator. Ignore in *nix as "\\" is a valid filename
    +     * 
    +     * @param  name the name to fix the separator for if running on Windows
    +     * @return      the fixed name
    +     */
    +    protected String handleWindowsSeparator(String name) {
    +        if (hostFsHasWindowsSeparator()) {
    +            return name.replace('\\', '/');
    +        }
    +        return name;
    +    }
    +
    +    protected boolean hostFsHasWindowsSeparator() {
    +        return OsUtils.isWin32();
    +    }
    +
         @Override
         public PathMatcher getPathMatcher(String syntaxAndPattern) {
             int colonIndex = Objects.requireNonNull(syntaxAndPattern, "No argument").indexOf(':');
    
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/IoUtils.java+152 8 modified
    @@ -47,6 +47,7 @@
     import java.util.Collection;
     import java.util.Collections;
     import java.util.EnumSet;
    +import java.util.Iterator;
     import java.util.List;
     import java.util.Objects;
     import java.util.Set;
    @@ -394,18 +395,13 @@ public static String getFileOwner(Path path, LinkOption... options) throws IOExc
          *                 explained above
          */
         public static Boolean checkFileExists(Path path, LinkOption... options) {
    -        boolean followLinks = true;
    -        for (LinkOption opt : options) {
    -            if (opt == LinkOption.NOFOLLOW_LINKS) {
    -                followLinks = false;
    -                break;
    -            }
    -        }
    +        boolean followLinks = followLinks(options);
    +
             try {
                 if (followLinks) {
                     path.getFileSystem().provider().checkAccess(path);
                 } else {
    -                Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
    +                Files.readAttributes(path, BasicFileAttributes.class, options);
                 }
                 return Boolean.TRUE;
             } catch (NoSuchFileException e) {
    @@ -415,6 +411,63 @@ public static Boolean checkFileExists(Path path, LinkOption... options) {
             }
         }
     
    +    /**
    +     * Checks that a file exists with or without following any symlinks.
    +     *
    +     * @param  path                the path to check
    +     * @param  neverFollowSymlinks whether to follow symlinks
    +     * @return                     true if the file exists with the symlink semantics, false if it doesn't exist, null
    +     *                             if symlinks were found, or it is unknown if whether the file exists
    +     */
    +    public static Boolean checkFileExistsAnySymlinks(Path path, boolean neverFollowSymlinks) {
    +        try {
    +            if (!neverFollowSymlinks) {
    +                path.getFileSystem().provider().checkAccess(path);
    +            } else {
    +                // this is a bad fix because this leaves a nasty race condition - the directory may turn into a symlink
    +                // between this check and the call to open()
    +                for (int i = 1; i <= path.getNameCount(); i++) {
    +                    Path checkForSymLink = getFirstPartsOfPath(path, i);
    +                    BasicFileAttributes basicFileAttributes
    +                            = Files.readAttributes(checkForSymLink, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
    +                    if (basicFileAttributes.isSymbolicLink()) {
    +                        return false;
    +                    }
    +                }
    +            }
    +            return true;
    +        } catch (NoSuchFileException e) {
    +            return false;
    +        } catch (IOException e) {
    +            return null;
    +        }
    +    }
    +
    +    /**
    +     * Extracts the first n parts of the path. For example <br>
    +     * ("/home/test/test12", 1) returns "/home", <br>
    +     * ("/home/test", 1) returns "/home/test" <br>
    +     * etc.
    +     *
    +     * @param  path           the path to extract parts of
    +     * @param  partsToExtract the number of parts to extract
    +     * @return                the extracted path
    +     */
    +    public static Path getFirstPartsOfPath(Path path, int partsToExtract) {
    +        String firstName = path.getName(0).toString();
    +        String[] names = new String[partsToExtract - 1];
    +        for (int j = 1; j < partsToExtract; j++) {
    +            names[j - 1] = path.getName(j).toString();
    +        }
    +        Path checkForSymLink = path.getFileSystem().getPath(firstName, names);
    +        // the root is not counted as a directory part so we must resolve the result relative to it.
    +        Path root = path.getRoot();
    +        if (root != null) {
    +            checkForSymLink = root.resolve(checkForSymLink);
    +        }
    +        return checkForSymLink;
    +    }
    +
         /**
          * Read the requested number of bytes or fail if there are not enough left.
          *
    @@ -637,4 +690,95 @@ public static List<String> readAllLines(BufferedReader reader, int lineCountHint
             }
             return result;
         }
    +
    +    /**
    +     * Chroot a path under the new root
    +     *
    +     * @param  newRoot    the new root
    +     * @param  toSanitize the path to sanitize and chroot
    +     * @return            the chrooted path under the newRoot filesystem
    +     */
    +    public static Path chroot(Path newRoot, Path toSanitize) {
    +        Objects.requireNonNull(newRoot);
    +        Objects.requireNonNull(toSanitize);
    +        List<String> sanitized = removeExtraCdUps(toSanitize);
    +        return buildPath(newRoot, newRoot.getFileSystem(), sanitized);
    +    }
    +
    +    /**
    +     * Remove any extra directory ups from the Path
    +     *
    +     * @param  toSanitize the path to sanitize
    +     * @return            the sanitized path
    +     */
    +    public static Path removeCdUpAboveRoot(Path toSanitize) {
    +        List<String> sanitized = removeExtraCdUps(toSanitize);
    +        return buildPath(toSanitize.getRoot(), toSanitize.getFileSystem(), sanitized);
    +    }
    +
    +    private static List<String> removeExtraCdUps(Path toResolve) {
    +        List<String> newNames = new ArrayList<>(toResolve.getNameCount());
    +
    +        int numCdUps = 0;
    +        int numDirParts = 0;
    +        for (int i = 0; i < toResolve.getNameCount(); i++) {
    +            String name = toResolve.getName(i).toString();
    +            if ("..".equals(name)) {
    +                // If we have more cdups than dir parts, so we ignore the ".." to avoid jail escapes
    +                if (numDirParts > numCdUps) {
    +                    ++numCdUps;
    +                    newNames.add(name);
    +                }
    +            } else {
    +                // if the current directory is a part of the name, don't increment number of dir parts, as it doesn't
    +                // add to the number of ".."s that can be present before the root
    +                if (!".".equals(name)) {
    +                    ++numDirParts;
    +                }
    +                newNames.add(name);
    +            }
    +        }
    +        return newNames;
    +    }
    +
    +    /**
    +     * Build a path from the list of path parts
    +     * 
    +     * @param  root      the root path
    +     * @param  fs        the filesystem
    +     * @param  namesList the parts of the path to build
    +     * @return           the built path
    +     */
    +    public static Path buildPath(Path root, FileSystem fs, List<String> namesList) {
    +        Objects.requireNonNull(fs);
    +        if (namesList == null) {
    +            return null;
    +        }
    +
    +        if (GenericUtils.isEmpty(namesList)) {
    +            return root == null ? fs.getPath(".") : root;
    +        }
    +
    +        Path cleanedPathToResolve = buildRelativePath(fs, namesList);
    +        return root == null ? cleanedPathToResolve : root.resolve(cleanedPathToResolve);
    +    }
    +
    +    /**
    +     * Build a relative path on the filesystem fs from the path parts in the namesList
    +     *
    +     * @param  fs        the filesystem for the path
    +     * @param  namesList the names list
    +     * @return           the built path
    +     */
    +    public static Path buildRelativePath(FileSystem fs, List<String> namesList) {
    +        String[] names = new String[namesList.size() - 1];
    +
    +        Iterator<String> it = namesList.iterator();
    +        String rootName = it.next();
    +        for (int i = 0; it.hasNext(); i++) {
    +            names[i] = it.next();
    +        }
    +        Path cleanedPathToResolve = fs.getPath(rootName, names);
    +        return cleanedPathToResolve;
    +    }
     }
    
  • sshd-common/src/test/java/org/apache/sshd/common/file/root/RootedFileSystemProviderTest.java+364 105 modified
    @@ -19,28 +19,29 @@
     
     package org.apache.sshd.common.file.root;
     
    +import java.io.File;
     import java.io.IOException;
    +import java.io.InputStream;
    +import java.io.OutputStream;
     import java.nio.ByteBuffer;
     import java.nio.channels.Channel;
     import java.nio.channels.FileChannel;
    -import java.nio.file.DirectoryStream;
    -import java.nio.file.FileSystem;
    -import java.nio.file.Files;
    -import java.nio.file.InvalidPathException;
    -import java.nio.file.OpenOption;
    -import java.nio.file.Path;
    -import java.nio.file.StandardCopyOption;
    -import java.nio.file.StandardOpenOption;
    +import java.nio.charset.StandardCharsets;
    +import java.nio.file.*;
    +import java.util.ArrayList;
     import java.util.Arrays;
     import java.util.Collections;
    +import java.util.List;
     import java.util.Objects;
     import java.util.Random;
     import java.util.TreeSet;
     
     import org.apache.sshd.common.util.GenericUtils;
    +import org.apache.sshd.common.util.OsUtils;
     import org.apache.sshd.util.test.CommonTestSupportUtils;
     import org.apache.sshd.util.test.NoIoTestCase;
    -import org.junit.BeforeClass;
    +import org.junit.Assert;
    +import org.junit.Assume;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
     import org.junit.experimental.categories.Category;
    @@ -49,7 +50,7 @@
     /**
      * Tests the RootedFileSystemProvider implementation of {@link java.nio.file.spi.FileSystemProvider} checking that
      * permissions for generic FS commands are not permitted outside of the root directory.
    - *
    + * <p>
      * Individual tests are form pairs (e.g. testX, testXInvalid) where testXInvalid is expected to test a parent path of
      * {@link RootedFileSystem#getRoot()}
      *
    @@ -58,19 +59,21 @@
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
     public class RootedFileSystemProviderTest extends AssertableFile {
    -    private static RootedFileSystem fileSystem;
    -    private static Path rootSandbox;
    +    private static final String SKIP_ON_WINDOWS = "Test fails due to windows normalizing paths before opening them, " +
    +                                                  "allowing one to open a file like \"C:\\directory_doesnt_exist\\..\\myfile.txt\" whereas this is blocked in unix";
    +    private static final String DOESNT_EXIST = "../doesnt_exist/../";
     
    -    public RootedFileSystemProviderTest() {
    -        super();
    -    }
    +    private final RootedFileSystem fileSystem;
    +    private final Path rootSandbox;
    +    private final FileHelper fileHelper;
     
    -    @BeforeClass
    -    public static void initializeFileSystem() throws IOException {
    +    public RootedFileSystemProviderTest() throws Exception {
    +        super();
    +        fileHelper = new FileHelper();
             Path targetFolder = Objects.requireNonNull(
                     CommonTestSupportUtils.detectTargetFolder(RootedFileSystemProviderTest.class),
                     "Failed to detect target folder");
    -        rootSandbox = FileHelper.createTestSandbox(targetFolder.resolve(TEMP_SUBFOLDER_NAME));
    +        rootSandbox = fileHelper.createTestSandbox(targetFolder.resolve(TEMP_SUBFOLDER_NAME));
             fileSystem = (RootedFileSystem) new RootedFileSystemProvider().newFileSystem(rootSandbox, Collections.emptyMap());
         }
     
    @@ -86,144 +89,216 @@ public void testRoot() {
         /* mkdir */
         @Test
         public void testMkdir() throws IOException {
    -        Path created = FileHelper.createDirectory(fileSystem.getPath(getCurrentTestName()));
    -        assertTrue(exists(created) && isDir(created) && isReadable(created));
    +        Path created = fileHelper.createDirectory(fileSystem.getPath(getCurrentTestName()));
    +        try {
    +            assertTrue(exists(created) && isDir(created) && isReadable(created));
    +        } finally {
    +            Files.delete(created);
    +        }
         }
     
    -    @Test(expected = InvalidPathException.class)
    -    public void testMkdirInvalid() throws IOException {
    -        Path parent = FileHelper.createDirectory(fileSystem.getPath("../" + getCurrentTestName()));
    -        fail(String.format("Unexpected success in creating directory %s", parent.toString()));
    +    @Test
    +    public void testMkdirInvalid() {
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +
    +        String parent = DOESNT_EXIST + getCurrentTestName();
    +        assertThrows(String.format("Unexpected success in creating directory %s", parent), NoSuchFileException.class,
    +                () -> fileHelper.createDirectory(fileSystem.getPath(parent)));
         }
     
         /* rmdir */
         @Test
         public void testRmdir() throws IOException {
    -        Path created = FileHelper.createDirectory(fileSystem.getPath(getCurrentTestName()));
    -        Path deleted = FileHelper.deleteDirectory(created);
    +        Path created = fileHelper.createDirectory(fileSystem.getPath(getCurrentTestName()));
    +        Path deleted = fileHelper.deleteDirectory(created);
             notExists(deleted);
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    @Test(expected = NoSuchFileException.class)
         public void testRmdirInvalid() throws IOException {
    -        Path deleted = FileHelper.deleteDirectory(fileSystem.getPath("../" + getCurrentTestName()));
    +        Path deleted = fileHelper.deleteDirectory(fileSystem.getPath(DOESNT_EXIST + getCurrentTestName()));
             fail(String.format("Unexpected success in removing directory %s", deleted.toString()));
         }
     
         /* chdir */
         @Test
         public void testChdir() throws IOException {
    -        Path created = FileHelper.createDirectory(fileSystem.getPath(getCurrentTestName()));
    -        Path createdFile = FileHelper.createFile(created.resolve(getCurrentTestName()));
    -        boolean hasFile = false;
    -        try (DirectoryStream<Path> ds = FileHelper.readDirectory(created)) {
    -            for (Path p : ds) {
    -                hasFile |= FileHelper.isSameFile(createdFile,
    -                        fileSystem.getPath(created.getFileName() + "/" + p.getFileName()));
    +        Path created = fileHelper.createDirectory(fileSystem.getPath(getCurrentTestName()));
    +        Path createdFile = fileHelper.createFile(created.resolve(getCurrentTestName()));
    +        try {
    +            boolean hasFile = false;
    +            try (DirectoryStream<Path> ds = fileHelper.readDirectory(created)) {
    +                for (Path p : ds) {
    +                    hasFile |= fileHelper.isSameFile(createdFile,
    +                            fileSystem.getPath(created.getFileName() + "/" + p.getFileName()));
    +                }
                 }
    +            assertTrue(createdFile + " found in ch directory", hasFile);
    +        } finally {
    +            Files.delete(createdFile);
    +            Files.delete(created);
             }
    -        assertTrue(createdFile + " found in ch directory", hasFile);
    -    }
    -
    -    @Test(expected = InvalidPathException.class)
    -    public void testChdirInvalid() throws IOException {
    -        Path chdir = FileHelper.createDirectory(fileSystem.getPath("../" + getCurrentTestName()));
    -        fail(String.format("Unexpected success in changing directory %s", chdir.toString()));
         }
     
         /* write */
         @Test
         public void testWriteFile() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
             assertTrue(exists(created) && isReadable(created));
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    @Test
         public void testWriteFileInvalid() throws IOException {
    -        Path written = FileHelper.createFile(fileSystem.getPath("../" + getCurrentTestName()));
    -        fail(String.format("Unexpected success in writing file %s", written.toString()));
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +
    +        String written = DOESNT_EXIST + getCurrentTestName();
    +        assertThrows(String.format("Unexpected success in writing file %s", written), NoSuchFileException.class,
    +                () -> fileHelper.createFile(fileSystem.getPath(written)));
         }
     
         /* read */
         @Test
         public void testReadFile() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    -        isNonEmpty(FileHelper.readFile(created));
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        isNonEmpty(fileHelper.readFile(created));
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    @Test(expected = NoSuchFileException.class)
         public void testReadFileInvalid() throws IOException {
    -        Path read = fileSystem.getPath("../" + getCurrentTestName());
    -        FileHelper.readFile(read);
    +        Path read = fileSystem.getPath(DOESNT_EXIST + getCurrentTestName());
    +        fileHelper.readFile(read);
             fail(String.format("Unexpected success in reading file %s", read.toString()));
         }
     
         /* rm */
         @Test
         public void testDeleteFile() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    -        Path deleted = FileHelper.deleteFile(created);
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        Path deleted = fileHelper.deleteFile(created);
             notExists(deleted);
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    @Test(expected = NoSuchFileException.class)
         public void testDeleteFileInvalid() throws IOException {
    -        Path deleted = FileHelper.deleteFile(fileSystem.getPath("../" + getCurrentTestName()));
    +        Path deleted = fileHelper.deleteFile(fileSystem.getPath(DOESNT_EXIST + getCurrentTestName()));
             fail(String.format("Unexpected success in deleting file %s", deleted.toString()));
         }
     
         /* cp */
         @Test
         public void testCopyFile() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
             Path destination = fileSystem.getPath(getCurrentTestName() + "dest");
    -        FileHelper.copyFile(created, destination);
    -        assertTrue(exists(destination) && isReadable(destination));
    +        try {
    +            fileHelper.copyFile(created, destination);
    +            assertTrue(exists(destination) && isReadable(destination));
    +        } finally {
    +            Files.delete(destination);
    +            Files.delete(created);
    +        }
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    @Test
         public void testCopyFileInvalid() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    -        Path copy = FileHelper.copyFile(created, fileSystem.getPath("../" + getCurrentTestName()));
    -        fail(String.format("Unexpected success in copying file to %s", copy.toString()));
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        String copy = DOESNT_EXIST + getCurrentTestName();
    +        assertThrows(String.format("Unexpected success in copying file to %s", copy),
    +                NoSuchFileException.class,
    +                () -> fileHelper.copyFile(created, fileSystem.getPath(copy)));
         }
     
         /* mv */
         @Test
         public void testMoveFile() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
             Path destination = fileSystem.getPath(getCurrentTestName() + "dest");
    -        FileHelper.moveFile(created, destination);
    +        fileHelper.moveFile(created, destination);
             assertTrue(notExists(created) && exists(destination) && isReadable(destination));
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    @Test
         public void testMoveFileInvalid() throws IOException {
    -        Path created = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    -        Path moved = FileHelper.moveFile(created, fileSystem.getPath("../" + getCurrentTestName()));
    -        fail(String.format("Unexpected success in moving file to %s", moved.toString()));
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +
    +        Path created = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        String moved = DOESNT_EXIST + getCurrentTestName();
    +        assertThrows(String.format("Unexpected success in moving file to %s", moved), NoSuchFileException.class,
    +                () -> fileHelper.moveFile(created, fileSystem.getPath(moved)));
         }
     
         /* link */
         @Test
         public void testCreateLink() throws IOException {
    -        Path existing = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        Path existing = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
             Path link = fileSystem.getPath(getCurrentTestName() + "link");
    -        FileHelper.createLink(link, existing);
    -        assertTrue(exists(link) && isReadable(link));
    +        try {
    +            fileHelper.createLink(link, existing);
    +            assertTrue(exists(link) && isReadable(link));
    +        } finally {
    +            Files.delete(link);
    +            Files.delete(existing);
    +        }
    +    }
    +
    +    @Test
    +    public void testJailbreakLink() throws IOException {
    +        testJailbreakLink("../");
    +    }
    +
    +    @Test
    +    public void testJailbreakLink2() throws IOException {
    +        testJailbreakLink("../test/");
    +    }
    +
    +    @Test
    +    public void testJailbreakLink3() throws IOException {
    +        testJailbreakLink("/..");
    +    }
    +
    +    @Test
    +    public void testJailbreakLink4() throws IOException {
    +        testJailbreakLink("/./..");
    +    }
    +
    +    @Test
    +    public void testJailbreakLink5() throws IOException {
    +        testJailbreakLink("/./../");
    +    }
    +
    +    @Test
    +    public void testJailbreakLink6() throws IOException {
    +        testJailbreakLink("./../");
    +    }
    +
    +    @Test
    +    public void testJailbreakLink7() throws IOException {
    +        String fileName = "/testdir/testdir2/../../..";
    +        testJailbreakLink(fileName);
         }
     
    -    @Test(expected = InvalidPathException.class)
    +    private void testJailbreakLink(String jailbrokenTarget) throws IOException {
    +        Path target = fileSystem.getPath(jailbrokenTarget);
    +        Path linkPath = fileSystem.getPath("/testLink");
    +        Assert.assertThrows(InvalidPathException.class, () -> fileSystem.provider().createSymbolicLink(linkPath, target));
    +        Assert.assertFalse(Files.exists(linkPath));
    +    }
    +
    +    @Test
         public void testCreateLinkInvalid() throws IOException {
    -        Path existing = FileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    -        Path link = FileHelper.createLink(fileSystem.getPath("../" + getCurrentTestName() + "link"), existing);
    -        fail(String.format("Unexpected success in linking file %s", link.toString()));
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +
    +        Path existing = fileHelper.createFile(fileSystem.getPath(getCurrentTestName()));
    +        String link = DOESNT_EXIST + getCurrentTestName() + "link";
    +        assertThrows(String.format("Unexpected success in linking file %s", link), NoSuchFileException.class,
    +                () -> fileHelper.createLink(fileSystem.getPath(link), existing));
         }
     
         @Test
         public void testNewByteChannelProviderMismatchException() throws IOException {
             RootedFileSystemProvider provider = new RootedFileSystemProvider();
    -        Path tempFolder = assertHierarchyTargetFolderExists(getTempTargetFolder());
    +        Path tempFolder = getTempTargetFolder();
             Path file = Files.createTempFile(tempFolder, getCurrentTestName(), ".txt");
             try (FileSystem fs = provider.newFileSystem(tempFolder, Collections.emptyMap());
                  Channel channel = provider.newByteChannel(fs.getPath(file.getFileName().toString()), Collections.emptySet())) {
    @@ -235,23 +310,212 @@ public void testNewByteChannelProviderMismatchException() throws IOException {
         public void testResolveRoot() throws IOException {
             Path root = GenericUtils.head(fileSystem.getRootDirectories());
             Path dir = root.resolve("tsd");
    -        FileHelper.createDirectory(dir);
    -        Path f1 = FileHelper.createFile(dir.resolve("test.txt"));
    -        Path f2 = Files.newDirectoryStream(dir).iterator().next();
    -        assertTrue("Unrooted path found", f2 instanceof RootedPath);
    -        assertEquals(f1, f2);
    -        FileHelper.deleteFile(f1);
    -        FileHelper.deleteDirectory(dir);
    +        fileHelper.createDirectory(dir);
    +        Path f1 = fileHelper.createFile(dir.resolve("test.txt"));
    +        try {
    +            Path f2;
    +            try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
    +                f2 = ds.iterator().next();
    +            }
    +            assertTrue("Unrooted path found", f2 instanceof RootedPath);
    +            assertEquals(f1, f2);
    +        } finally {
    +            fileHelper.deleteFile(f1);
    +            fileHelper.deleteDirectory(dir);
    +        }
    +    }
    +
    +    @Test
    +    public void testBreakOutOfChroot1() throws IOException {
    +        String fileName = "../" + getCurrentTestName();
    +        testBreakOutOfChroot(fileName, fileName);
    +    }
    +
    +    @Test
    +    public void testBreakOutOfChroot2() throws IOException {
    +        String fileName = "./../" + getCurrentTestName();
    +        testBreakOutOfChroot(fileName, fileName);
    +    }
    +
    +    @Test
    +    public void testBreakOutOfChroot3() throws IOException {
    +        String fileName = "/../" + getCurrentTestName();
    +        testBreakOutOfChroot(fileName, fileName);
    +    }
    +
    +    @Test
    +    public void testBreakOutOfChroot4() throws IOException {
    +        String fileName = "/.././" + getCurrentTestName();
    +        testBreakOutOfChroot(fileName, fileName);
    +    }
    +
    +    @Test
    +    public void testBreakOutOfChroot5() throws IOException {
    +        String fileName = "/./../" + getCurrentTestName();
    +        testBreakOutOfChroot(fileName, fileName);
    +    }
    +
    +    @Test
    +    public void testBreakOutOfChroot6() throws IOException {
    +        String fileName = "//../" + getCurrentTestName();
    +        testBreakOutOfChroot(fileName, "/../" + getCurrentTestName());
    +    }
    +
    +    /**
    +     * Tests to make sure that the attempted break out of the chroot does not work with the specified filename
    +     *
    +     * @param  fileName    the filename to attempt to break out of the chroot with
    +     * @throws IOException on test failure
    +     */
    +    private void testBreakOutOfChroot(String fileName, String expected) throws IOException {
    +        RootedPath breakoutAttempt = fileSystem.getPath(fileName);
    +
    +        // make sure that our rooted fs behaves like a proper unix fs
    +        Assert.assertEquals(expected, breakoutAttempt.toString());
    +
    +        Path expectedDir = fileSystem.getRoot().resolve(getCurrentTestName());
    +        Path newDir = fileHelper.createDirectory(breakoutAttempt);
    +        try {
    +            assertTrue(Files.isDirectory(expectedDir));
    +
    +            String baseName = breakoutAttempt.getName(breakoutAttempt.getNameCount() - 1).toString();
    +            assertTrue(fileHelper.isSameFile(newDir, fileSystem.getPath(baseName)));
    +
    +            // make sure we didn't create it one directory out of the jail
    +            assertFalse(Files.exists(fileSystem.getRoot().resolve("../" + breakoutAttempt.getFileName().toString())));
    +
    +            // make sure various methods of referencing the file work.
    +            assertTrue(fileHelper.isSameFile(newDir, fileSystem.getPath("/" + baseName)));
    +            assertTrue(fileHelper.isSameFile(newDir, fileSystem.getPath("/../../" + baseName)));
    +            assertTrue(fileHelper.isSameFile(newDir, fileSystem.getPath("./../" + baseName)));
    +        } finally {
    +            // cleanup the directory.
    +            fileHelper.deleteDirectory(newDir);
    +        }
    +
    +        assertFalse(Files.isDirectory(expectedDir));
    +        assertFalse(Files.isDirectory(newDir));
    +    }
    +
    +    @Test
    +    public void testValidSymlink1() throws IOException {
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +        String fileName = "/testdir/../";
    +        testValidSymlink(fileName, true);
    +    }
    +
    +    @Test
    +    public void testValidSymlink2() throws IOException {
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +        String fileName = "/testdir/testdir2/../";
    +        testValidSymlink(fileName, true);
    +    }
    +
    +    @Test
    +    public void testValidSymlink3() throws IOException {
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +        String fileName = "/testdir/../testdir3/";
    +        testValidSymlink(fileName, true);
    +    }
    +
    +    @Test
    +    public void testValidSymlink4() throws IOException {
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +        String fileName = "testdir/../testdir3/../";
    +        testValidSymlink(fileName, true);
    +    }
    +
    +    @Test
    +    public void testValidSymlink5() throws IOException {
    +        Assume.assumeFalse(SKIP_ON_WINDOWS, OsUtils.isWin32());
    +        String fileName = "testdir/../testdir3/../testfile";
    +        testValidSymlink(fileName, false);
    +    }
    +
    +    public void testValidSymlink(String symlink, boolean targetIsDirectory) throws IOException {
    +        Path target = fileSystem.getPath(symlink);
    +        Path linkPath = fileSystem.getPath("/testLink");
    +        final List<Path> toDelete = new ArrayList<>();
    +        try {
    +            fileSystem.provider().createSymbolicLink(linkPath, target);
    +            toDelete.add(linkPath);
    +
    +            // ensure that nothing processed the symlink.
    +            Assert.assertEquals(Paths.get(symlink).toString(),
    +                    fileSystem.provider().readSymbolicLink(linkPath).toString());
    +            Assert.assertFalse(Files.exists(target));
    +            Assert.assertEquals(Files.exists(linkPath), Files.exists(target));
    +
    +            // If we don't follow the link, we simply check that the link exists, which it does as we created it.
    +            Assert.assertTrue(Files.exists(linkPath, LinkOption.NOFOLLOW_LINKS));
    +
    +            createParentDirs(targetIsDirectory ? target : target.getParent(), toDelete);
    +
    +            if (!targetIsDirectory) {
    +                Files.createFile(target);
    +                toDelete.add(target);
    +            }
    +
    +            Assert.assertTrue(Files.exists(linkPath));
    +        } finally {
    +            for (int i = toDelete.size() - 1; i >= 0; i--) {
    +                Path path = toDelete.get(i);
    +                try {
    +                    Files.delete(path);
    +                } catch (IOException ex) {
    +                    // ignore as we might try to delete "/dir/.." which will fail as it contains dir..
    +                }
    +            }
    +        }
    +    }
    +
    +    private static void createParentDirs(Path target, List<Path> toDelete) throws IOException {
    +        if (target.getParent() != null) {
    +            createParentDirs(target.getParent(), toDelete);
    +        }
    +
    +        if (!Files.isDirectory(target)) {
    +            Files.createDirectories(target);
    +            toDelete.add(target);
    +        }
    +    }
    +
    +    @Test
    +    public void testFileNamedSlashOnUnixBasedOS() throws IOException {
    +        // skip ths test on Win32
    +        if (!"\\".equals(File.separator)) {
    +            Path slashFile = fileSystem.getPath("\\");
    +            Path created = fileHelper.createFile(slashFile);
    +            try {
    +                assertTrue(Files.isRegularFile(created));
    +            } finally {
    +                fileHelper.deleteFile(created);
    +            }
    +        }
    +    }
    +
    +    @Test
    +    public void testStreams() throws IOException {
    +        byte[] data = "This is test data".getBytes(StandardCharsets.UTF_8);
    +        RootedPath testPath = fileSystem.getPath("testfile.txt");
    +        try (OutputStream is = Files.newOutputStream(testPath)) {
    +            is.write(data);
    +        }
    +        byte[] read = new byte[data.length];
    +        try (InputStream is = Files.newInputStream(testPath)) {
    +            is.read(read);
    +        }
    +        assertArrayEquals(data, read);
         }
     
         /* Private helper */
     
         /**
          * Wrapper around the FileSystemProvider to test generic FS related commands. All created temp directories and files
    -     * used for testing are deleted upon JVM exit.
    +     * used for testing must be deleted in the test which creates them
          */
         @SuppressWarnings("synthetic-access")
    -    private static final class FileHelper {
    +    private final class FileHelper {
             private FileHelper() {
                 super();
             }
    @@ -263,71 +527,66 @@ private FileHelper() {
              * @return             the created sandbox Path
              * @throws IOException on failure to create
              */
    -        public static Path createTestSandbox(Path tempDir) throws IOException {
    -            Path created = Files.createDirectories(tempDir.resolve(RootedFileSystemProviderTest.class.getSimpleName()));
    -            created.toFile().deleteOnExit();
    +        public Path createTestSandbox(Path tempDir) throws IOException {
    +            Path path = tempDir.resolve(RootedFileSystemProviderTest.class.getSimpleName());
    +            Path created = Files.createDirectories(path);
                 return created;
             }
     
    -        public static Path createFile(Path source) throws InvalidPathException, IOException {
    +        public Path createFile(Path source) throws InvalidPathException, IOException {
                 try (FileChannel fc = fileSystem.provider().newFileChannel(source,
                         new TreeSet<OpenOption>(Arrays.asList(StandardOpenOption.CREATE, StandardOpenOption.WRITE)))) {
                     byte[] randomBytes = new byte[1000];
                     new Random().nextBytes(randomBytes);
                     fc.write(ByteBuffer.wrap(randomBytes));
    -                source.toFile().deleteOnExit();
                     return source;
                 }
             }
     
    -        public static Path createLink(Path link, Path existing) throws IOException {
    +        public Path createLink(Path link, Path existing) throws IOException {
                 fileSystem.provider().createLink(link, existing);
    -            link.toFile().deleteOnExit();
                 return link;
             }
     
    -        public static Path createDirectory(Path dir) throws InvalidPathException, IOException {
    +        public Path createDirectory(Path dir) throws InvalidPathException, IOException {
                 fileSystem.provider().createDirectory(dir);
    -            dir.toFile().deleteOnExit();
                 return dir;
             }
     
    -        public static Path deleteDirectory(Path dir) throws InvalidPathException, IOException {
    +        public Path deleteDirectory(Path dir) throws InvalidPathException, IOException {
                 return deleteFile(dir);
             }
     
    -        public static Path deleteFile(Path source) throws InvalidPathException, IOException {
    +        public Path deleteFile(Path source) throws InvalidPathException, IOException {
                 fileSystem.provider().delete(source);
                 return source;
             }
     
    -        public static byte[] readFile(Path source) throws IOException {
    +        public byte[] readFile(Path source) throws IOException {
                 try (FileChannel fc = fileSystem.provider().newFileChannel(source,
    -                    new TreeSet<OpenOption>(Arrays.asList(StandardOpenOption.READ)))) {
    -                byte[] readBytes = new byte[(int) source.toFile().length()];
    +                    Collections.singleton(StandardOpenOption.READ))) {
    +                byte[] readBytes = new byte[(int) Files.size(source)];
                     fc.read(ByteBuffer.wrap(readBytes));
                     return readBytes;
                 }
             }
     
    -        public static Path copyFile(Path source, Path destination) throws InvalidPathException, IOException {
    +        public Path copyFile(Path source, Path destination) throws InvalidPathException, IOException {
                 fileSystem.provider().copy(source, destination, StandardCopyOption.COPY_ATTRIBUTES);
    -            destination.toFile().deleteOnExit();
                 return destination;
             }
     
    -        public static Path moveFile(Path source, Path destination) throws InvalidPathException, IOException {
    +        public Path moveFile(Path source, Path destination) throws InvalidPathException, IOException {
                 fileSystem.provider().move(source, destination, StandardCopyOption.ATOMIC_MOVE);
    -            destination.toFile().deleteOnExit();
                 return destination;
             }
     
    -        public static DirectoryStream<Path> readDirectory(Path dir) throws InvalidPathException, IOException {
    +        public DirectoryStream<Path> readDirectory(Path dir) throws InvalidPathException, IOException {
                 DirectoryStream<Path> dirStream = fileSystem.provider().newDirectoryStream(dir, entry -> true);
                 return dirStream;
             }
     
    -        public static boolean isSameFile(Path source, Path destination) throws IOException {
    +        public boolean isSameFile(Path source, Path destination) throws IOException {
                 return fileSystem.provider().isSameFile(source, destination);
             }
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/io/IoUtilsTest.java+48 0 modified
    @@ -19,11 +19,17 @@
     
     package org.apache.sshd.common.util.io;
     
    +import java.io.IOException;
    +import java.nio.file.Files;
     import java.nio.file.LinkOption;
    +import java.nio.file.Path;
    +import java.nio.file.Paths;
     
     import org.apache.sshd.common.util.NumberUtils;
    +import org.apache.sshd.util.test.CommonTestSupportUtils;
     import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
    +import org.junit.Assert;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
     import org.junit.experimental.categories.Category;
    @@ -58,4 +64,46 @@ public void testGetEOLBytes() {
             }
         }
     
    +    /**
    +     * Tests to make sure check exists does not follow symlinks.
    +     *
    +     * @throws IOException on failure
    +     */
    +    @Test
    +    public void testCheckExists() throws IOException {
    +        testCheckExists(Paths.get("target/IoUtilsTest").toAbsolutePath());
    +    }
    +
    +    public void testCheckExists(Path baseDir) throws IOException {
    +        CommonTestSupportUtils.deleteRecursive(baseDir, LinkOption.NOFOLLOW_LINKS);
    +
    +        Path folder = baseDir.resolve("folder1/folder2/");
    +        Files.createDirectories(folder);
    +
    +        Path target = baseDir.resolve("folder1/target");
    +        Files.createDirectories(target);
    +
    +        Path dirInTarget = baseDir.resolve("folder1/target/dirintarget");
    +        Files.createDirectories(dirInTarget);
    +
    +        Files.createDirectories(target);
    +        Path link = baseDir.resolve("folder1/folder2/link");
    +        Files.createSymbolicLink(link, target);
    +
    +        Path link2 = baseDir.resolve("link");
    +        Files.createSymbolicLink(link2, target);
    +
    +        Path targetWithLink = baseDir.resolve("folder1/folder2/link/dirintarget");
    +
    +        Assert.assertTrue("symlink follow should work", IoUtils.checkFileExists(targetWithLink));
    +        Assert.assertTrue("symlink follow should work", IoUtils.checkFileExistsAnySymlinks(targetWithLink, false));
    +
    +        Assert.assertFalse("Link at end shouldn't be followed", IoUtils.checkFileExistsAnySymlinks(link, true));
    +        Assert.assertFalse("Nofollow shouldn't follow directory",
    +                IoUtils.checkFileExistsAnySymlinks(targetWithLink, true));
    +        Assert.assertFalse("Link at beginning shouldn't be followed",
    +                IoUtils.checkFileExistsAnySymlinks(link2, true));
    +        Assert.assertTrue("Root directory must exist",
    +                IoUtils.checkFileExistsAnySymlinks(baseDir, true));
    +    }
     }
    
  • sshd-common/src/test/java/org/apache/sshd/util/test/CommonTestSupportUtils.java+2 2 modified
    @@ -495,11 +495,11 @@ private static Path getFile(String resource) {
          * @throws IOException If failed to access/remove some file(s)
          */
         public static Path deleteRecursive(Path path, LinkOption... options) throws IOException {
    -        if ((path == null) || (!Files.exists(path))) {
    +        if ((path == null) || (!Files.exists(path, options))) {
                 return path;
             }
     
    -        if (Files.isDirectory(path)) {
    +        if (Files.isDirectory(path, options)) {
                 try (DirectoryStream<Path> ds = Files.newDirectoryStream(path)) {
                     for (Path child : ds) {
                         deleteRecursive(child, options);
    
  • sshd-sftp/src/main/java/org/apache/sshd/sftp/client/fs/SftpFileSystemProvider.java+2 2 modified
    @@ -621,7 +621,7 @@ public void copy(Path source, Path target, CopyOption... options) throws IOExcep
             }
     
             // delete target if it exists and REPLACE_EXISTING is specified
    -        Boolean status = IoUtils.checkFileExists(target, linkOptions);
    +        Boolean status = IoUtils.checkFileExistsAnySymlinks(target, noFollowLinks);
             if (status == null) {
                 throw new AccessDeniedException("Existence cannot be determined for copy target: " + target);
             }
    @@ -698,7 +698,7 @@ public void move(Path source, Path target, CopyOption... options) throws IOExcep
             }
     
             // delete target if it exists and REPLACE_EXISTING is specified
    -        Boolean status = IoUtils.checkFileExists(target, linkOptions);
    +        Boolean status = IoUtils.checkFileExistsAnySymlinks(target, noFollowLinks);
             if (status == null) {
                 throw new AccessDeniedException("Existence cannot be determined for move target " + target);
             }
    
  • sshd-sftp/src/main/java/org/apache/sshd/sftp/server/AbstractSftpSubsystemHelper.java+75 26 modified
    @@ -36,6 +36,7 @@
     import java.nio.file.LinkOption;
     import java.nio.file.NoSuchFileException;
     import java.nio.file.NotDirectoryException;
    +import java.nio.file.OpenOption;
     import java.nio.file.Path;
     import java.nio.file.StandardCopyOption;
     import java.nio.file.StandardOpenOption;
    @@ -51,7 +52,6 @@
     import java.util.Collection;
     import java.util.Collections;
     import java.util.Comparator;
    -import java.util.EnumSet;
     import java.util.HashSet;
     import java.util.LinkedHashMap;
     import java.util.LinkedList;
    @@ -645,9 +645,11 @@ protected Map<String, Object> doLStat(int id, String path, int flags) throws IOE
              * SSH_FXP_LSTAT does not.
              */
             SftpFileSystemAccessor accessor = getFileSystemAccessor();
    +
    +        boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_LSTAT, "", p);
             LinkOption[] options = accessor.resolveFileAccessLinkOptions(
                     this, p, SftpConstants.SSH_FXP_LSTAT, "", false);
    -        return resolveFileAttributes(p, flags, options);
    +        return resolveFileAttributes(p, flags, !followLinks, options);
         }
     
         protected void doSetStat(
    @@ -675,7 +677,7 @@ protected void doSetStat(
     
             Path p = resolveFile(path);
             if (followLinks == null) {
    -            followLinks = resolvePathResolutionFollowLinks(cmd, extension, p);
    +            followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_SETSTAT, extension, p);
             }
             doSetAttributes(cmd, extension, p, attrs, followLinks);
         }
    @@ -1408,12 +1410,14 @@ protected Map<String, Object> doStat(int id, String path, int flags) throws IOEx
              */
             Path p = resolveFile(path);
             SftpFileSystemAccessor accessor = getFileSystemAccessor();
    +        boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_STAT, "", p);
             LinkOption[] options = accessor.resolveFileAccessLinkOptions(
    -                this, p, SftpConstants.SSH_FXP_STAT, "", true);
    -        return resolveFileAttributes(p, flags, options);
    +                this, p, SftpConstants.SSH_FXP_STAT, "", followLinks);
    +        return resolveFileAttributes(p, flags, !followLinks, options);
         }
     
         protected void doRealPath(Buffer buffer, int id) throws IOException {
    +        // do things here.
             String path = buffer.getString();
             boolean debugEnabled = log.isDebugEnabled();
             ServerSession session = getServerSession();
    @@ -1467,7 +1471,6 @@ protected void doRealPath(Buffer buffer, int id) throws IOException {
                     result = doRealPathV6(id, path, extraPaths, p, options);
     
                     p = result.getKey();
    -                options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p);
                     Boolean status = result.getValue();
                     switch (control) {
                         case SftpConstants.SSH_FXP_REALPATH_STAT_IF:
    @@ -1557,14 +1560,14 @@ protected SimpleImmutableEntry<Path, Boolean> validateRealPath(
                 int id, String path, Path f, LinkOption... options)
                 throws IOException {
             Path p = normalize(f);
    -        Boolean status = IoUtils.checkFileExists(p, options);
    +        Boolean status = IoUtils.checkFileExistsAnySymlinks(p, !IoUtils.followLinks(options));
             return new SimpleImmutableEntry<>(p, status);
         }
     
         protected void doRemoveDirectory(Buffer buffer, int id) throws IOException {
             String path = buffer.getString();
             try {
    -            doRemoveDirectory(id, path, false);
    +            doRemoveDirectory(id, path);
             } catch (IOException | RuntimeException e) {
                 sendStatus(prepareReply(buffer), id, e,
                         SftpConstants.SSH_FXP_RMDIR, path);
    @@ -1574,15 +1577,23 @@ protected void doRemoveDirectory(Buffer buffer, int id) throws IOException {
             sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, "");
         }
     
    -    protected void doRemoveDirectory(int id, String path, boolean followLinks) throws IOException {
    +    protected void doRemoveDirectory(int id, String path) throws IOException {
             Path p = resolveFile(path);
             if (log.isDebugEnabled()) {
                 log.debug("doRemoveDirectory({})[id={}] SSH_FXP_RMDIR (path={})[{}]", getServerSession(), id, path, p);
             }
     
             SftpFileSystemAccessor accessor = getFileSystemAccessor();
    +
    +        final boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_RMDIR, "", p);
    +        Boolean symlinkCheck = validateParentExistWithNoSymlinksIfNeverFollowSymlinks(p, !followLinks);
    +        if (!Boolean.TRUE.equals(symlinkCheck)) {
    +            throw new AccessDeniedException(p.toString(), p.toString(),
    +                    "Parent directories do not exist ore are prohibited symlinks");
    +        }
    +
             LinkOption[] options = accessor.resolveFileAccessLinkOptions(
    -                this, p, SftpConstants.SSH_FXP_RMDIR, "", followLinks);
    +                this, p, SftpConstants.SSH_FXP_RMDIR, "", false);
             if (Files.isDirectory(p, options)) {
                 doRemove(id, p, true);
             } else {
    @@ -1618,7 +1629,7 @@ protected void doMakeDirectory(Buffer buffer, int id) throws IOException {
             String path = buffer.getString();
             Map<String, ?> attrs = readAttrs(buffer);
             try {
    -            doMakeDirectory(id, path, attrs, false);
    +            doMakeDirectory(id, path, attrs);
             } catch (IOException | RuntimeException e) {
                 sendStatus(prepareReply(buffer), id, e,
                         SftpConstants.SSH_FXP_MKDIR, path, attrs);
    @@ -1629,7 +1640,7 @@ protected void doMakeDirectory(Buffer buffer, int id) throws IOException {
         }
     
         protected void doMakeDirectory(
    -            int id, String path, Map<String, ?> attrs, boolean followLinks)
    +            int id, String path, Map<String, ?> attrs)
                 throws IOException {
             Path resolvedPath = resolveFile(path);
             ServerSession session = getServerSession();
    @@ -1640,14 +1651,21 @@ protected void doMakeDirectory(
     
             SftpFileSystemAccessor accessor = getFileSystemAccessor();
             LinkOption[] options = accessor.resolveFileAccessLinkOptions(
    -                this, resolvedPath, SftpConstants.SSH_FXP_MKDIR, "", followLinks);
    +                this, resolvedPath, SftpConstants.SSH_FXP_MKDIR, "", false);
    +        final boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_MKDIR, "", resolvedPath);
             SftpPathImpl.withAttributeCache(resolvedPath, p -> {
    -            Boolean status = IoUtils.checkFileExists(p, options);
    -            if (status == null) {
    +            Boolean symlinkCheck = validateParentExistWithNoSymlinksIfNeverFollowSymlinks(p, !followLinks);
    +            if (!Boolean.TRUE.equals(symlinkCheck)) {
    +                throw new AccessDeniedException(p.toString(), p.toString(),
    +                        "Parent directories do not exist ore are prohibited symlinks");
    +            }
    +
    +            Boolean fileExists = IoUtils.checkFileExists(p, options);
    +            if (fileExists == null) {
                     throw new AccessDeniedException(p.toString(), p.toString(), "Cannot validate make-directory existence");
                 }
     
    -            if (status) {
    +            if (fileExists) {
                     if (Files.isDirectory(p, options)) {
                         throw new FileAlreadyExistsException(p.toString(), p.toString(), "Target directory already exists");
                     } else {
    @@ -1661,7 +1679,6 @@ protected void doMakeDirectory(
             listener.creating(session, resolvedPath, attrs);
             try {
                 accessor.createDirectory(this, resolvedPath);
    -            followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_MKDIR, "", resolvedPath);
                 doSetAttributes(SftpConstants.SSH_FXP_MKDIR, "", resolvedPath, attrs, followLinks);
             } catch (IOException | RuntimeException | Error e) {
                 listener.created(session, resolvedPath, attrs, e);
    @@ -1676,7 +1693,7 @@ protected void doRemove(Buffer buffer, int id) throws IOException {
                 /*
                  * If 'filename' is a symbolic link, the link is removed, not the file it points to.
                  */
    -            doRemoveFile(id, path, false);
    +            doRemoveFile(id, path);
             } catch (IOException | RuntimeException e) {
                 sendStatus(prepareReply(buffer), id, e, SftpConstants.SSH_FXP_REMOVE, path);
                 return;
    @@ -1685,17 +1702,20 @@ protected void doRemove(Buffer buffer, int id) throws IOException {
             sendStatus(prepareReply(buffer), id, SftpConstants.SSH_FX_OK, "");
         }
     
    -    protected void doRemoveFile(int id, String path, boolean followLinks) throws IOException {
    +    protected void doRemoveFile(int id, String path) throws IOException {
             Path resolvedPath = resolveFile(path);
             if (log.isDebugEnabled()) {
                 log.debug("doRemoveFile({})[id={}] SSH_FXP_REMOVE (path={}[{}])", getServerSession(), id, path, resolvedPath);
             }
    +        // whether to follow links in the dir up to the final file
    +        boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_REMOVE, "", resolvedPath);
     
             SftpFileSystemAccessor accessor = getFileSystemAccessor();
    +        // never resolve links in the final path to remove as we want to remove the symlink, not the target
             LinkOption[] options = accessor.resolveFileAccessLinkOptions(
    -                this, resolvedPath, SftpConstants.SSH_FXP_REMOVE, "", followLinks);
    +                this, resolvedPath, SftpConstants.SSH_FXP_REMOVE, "", false);
             SftpPathImpl.withAttributeCache(resolvedPath, p -> {
    -            Boolean status = IoUtils.checkFileExists(p, options);
    +            Boolean status = checkSymlinkState(p, followLinks, options);
                 if (status == null) {
                     throw signalRemovalPreConditionFailure(id, path, p,
                             new AccessDeniedException(p.toString(), p.toString(), "Cannot determine existence of remove candidate"),
    @@ -2293,8 +2313,9 @@ protected void writeDirEntry(
                 int id, DirectoryHandle dir, Map<String, Path> entries, Buffer buffer,
                 int index, Path f, String shortName, LinkOption... options)
                 throws IOException {
    +        boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_READDIR, "", f);
             Map<String, ?> attrs = resolveFileAttributes(
    -                f, SftpConstants.SSH_FILEXFER_ATTR_ALL, options);
    +                f, SftpConstants.SSH_FILEXFER_ATTR_ALL, !followLinks, options);
             entries.put(shortName, f);
     
             SftpFileSystemAccessor accessor = getFileSystemAccessor();
    @@ -2392,10 +2413,10 @@ protected String getShortName(Path f) throws IOException {
         }
     
         protected NavigableMap<String, Object> resolveFileAttributes(
    -            Path path, int flags, LinkOption... options)
    +            Path path, int flags, boolean neverFollowSymLinks, LinkOption... options)
                 throws IOException {
             return SftpPathImpl.withAttributeCache(path, file -> {
    -            Boolean status = IoUtils.checkFileExists(file, options);
    +            Boolean status = checkSymlinkState(file, neverFollowSymLinks, options);
                 if (status == null) {
                     return handleUnknownStatusFileAttributes(file, flags, options);
                 } else if (!status) {
    @@ -2406,7 +2427,31 @@ protected NavigableMap<String, Object> resolveFileAttributes(
             });
         }
     
    -    protected void writeAttrs(Buffer buffer, Map<String, ?> attributes) throws IOException {
    +    /**
    +     * A utility function to validate that the directories leading up to a file are not symlinks
    +     *
    +     * @param  path                the file to check for symlink presence
    +     * @param  neverFollowSymLinks whether to never follow symlinks in the parent paths
    +     * @param  options             whether the file itself can be a symlink
    +     * @return                     whether there are symlinks in the path to this file, or null if unknown
    +     */
    +    public Boolean checkSymlinkState(Path path, boolean neverFollowSymLinks, LinkOption[] options) {
    +        Boolean status = validateParentExistWithNoSymlinksIfNeverFollowSymlinks(path, neverFollowSymLinks);
    +        if (!Boolean.FALSE.equals(status)) {
    +            status = IoUtils.checkFileExists(path, options);
    +        }
    +        return status;
    +    }
    +
    +    public Boolean validateParentExistWithNoSymlinksIfNeverFollowSymlinks(Path path, boolean neverFollowSymLinks) {
    +        Boolean status = true;
    +        if (neverFollowSymLinks && path.getParent() != null) {
    +            status = IoUtils.checkFileExistsAnySymlinks(path.getParent(), true);
    +        }
    +        return status;
    +    }
    +
    +    protected void writeAttrs(Buffer buffer, Map<String, ?> attributes) {
             SftpHelper.writeAttrs(buffer, getVersion(), attributes);
         }
     
    @@ -2653,7 +2698,11 @@ protected void setFileAttributes(
                     case IoUtils.SIZE_VIEW_ATTR: {
                         long newSize = ((Number) value).longValue();
                         SftpFileSystemAccessor accessor = getFileSystemAccessor();
    -                    Set<StandardOpenOption> openOptions = EnumSet.of(StandardOpenOption.WRITE);
    +                    Set<OpenOption> openOptions = new HashSet<>();
    +                    openOptions.add(StandardOpenOption.WRITE);
    +                    if (!IoUtils.followLinks(options)) {
    +                        openOptions.add(LinkOption.NOFOLLOW_LINKS);
    +                    }
                         try (SeekableByteChannel channel = accessor.openFile(this, null, file, null, openOptions)) {
                             channel.truncate(newSize);
                             accessor.closeFile(this, null, file, null, channel, openOptions);
    
  • sshd-sftp/src/main/java/org/apache/sshd/sftp/server/SftpFileSystemAccessor.java+83 11 modified
    @@ -19,20 +19,14 @@
     
     package org.apache.sshd.sftp.server;
     
    +import java.io.FileNotFoundException;
     import java.io.IOException;
     import java.io.StreamCorruptedException;
     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.*;
     import java.nio.file.attribute.AclEntry;
     import java.nio.file.attribute.AclFileAttributeView;
     import java.nio.file.attribute.FileAttribute;
    @@ -44,8 +38,10 @@
     import java.nio.file.attribute.UserPrincipalLookupService;
     import java.nio.file.attribute.UserPrincipalNotFoundException;
     import java.security.Principal;
    +import java.util.Arrays;
     import java.util.Collection;
     import java.util.Collections;
    +import java.util.HashSet;
     import java.util.List;
     import java.util.Map;
     import java.util.NavigableMap;
    @@ -214,7 +210,11 @@ default SeekableByteChannel openFile(
                 attrs = IoUtils.EMPTY_FILE_ATTRIBUTES;
             }
     
    -        return FileChannel.open(file, options, attrs);
    +        // Don't use Set contains as this can fail with TreeSet
    +        if (!Arrays.asList(options.toArray(new OpenOption[0])).contains(LinkOption.NOFOLLOW_LINKS)) {
    +            return FileChannel.open(file, options, attrs);
    +        }
    +        return seekableByteChannelNoLinkFollow(file, options, attrs);
         }
     
         /**
    @@ -317,9 +317,12 @@ default void closeFile(
          * @throws IOException If failed to open
          */
         default DirectoryStream<Path> openDirectory(
    -            SftpSubsystemProxy subsystem, DirectoryHandle dirHandle, Path dir, String handle)
    +            SftpSubsystemProxy subsystem, DirectoryHandle dirHandle, Path dir, String handle, LinkOption... linkOptions)
                 throws IOException {
    -        return Files.newDirectoryStream(dir);
    +        if (IoUtils.followLinks(linkOptions)) {
    +            return Files.newDirectoryStream(dir);
    +        }
    +        return secureResolveDirectoryStream(dir);
         }
     
         /**
    @@ -523,15 +526,84 @@ default void renameFile(
         default void copyFile(
                 SftpSubsystemProxy subsystem, Path src, Path dst, Collection<CopyOption> opts)
                 throws IOException {
    +
    +        if (noFollow(opts)) {
    +            try (SeekableByteChannel srcFile
    +                    = seekableByteChannelNoLinkFollow(src, Collections.singleton(StandardOpenOption.READ))) {
    +                if (!(srcFile instanceof FileChannel)) {
    +                    throw new UnsupportedOperationException("Host file system must return a file channel");
    +                }
    +
    +                try (SeekableByteChannel dstFile = seekableByteChannelNoLinkFollow(src, new HashSet<>(Arrays
    +                        .asList(StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)))) {
    +                    ((FileChannel) srcFile).transferTo(0, srcFile.size(), dstFile);
    +                }
    +            }
    +            return;
    +        }
             Files.copy(src, dst,
                     GenericUtils.isEmpty(opts)
                             ? IoUtils.EMPTY_COPY_OPTIONS
                             : opts.toArray(new CopyOption[opts.size()]));
         }
     
    +    static SeekableByteChannel seekableByteChannelNoLinkFollow(
    +            Path src, Set<? extends OpenOption> opts, FileAttribute<?>... fileAttributes)
    +            throws IOException {
    +        if (src.getNameCount() < 1) {
    +            // opening root directory isn't supported.
    +            throw new IllegalArgumentException();
    +        }
    +        Path toResolve = src.isAbsolute() ? src : src.getFileSystem().getPath(src.toString());
    +
    +        if (!Files.isDirectory(src.getParent(), LinkOption.NOFOLLOW_LINKS)) {
    +            throw new FileNotFoundException(src.getParent().toString());
    +        }
    +
    +        toResolve = toResolve.normalize();
    +        try (SecureDirectoryStream<Path> ds = secureResolveDirectoryStream(toResolve.getParent())) {
    +            Set<OpenOption> newOpts = new HashSet<>(opts);
    +            newOpts.add(LinkOption.NOFOLLOW_LINKS);
    +            return ds.newByteChannel(toResolve.getName(toResolve.getNameCount() - 1), newOpts, fileAttributes);
    +        }
    +    }
    +
    +    static SecureDirectoryStream<Path> secureResolveDirectoryStream(Path toResolve) throws IOException {
    +        toResolve = IoUtils.removeCdUpAboveRoot(toResolve);
    +        DirectoryStream<Path> ds = Files.newDirectoryStream(toResolve.getRoot());
    +        for (int i = 0; i < toResolve.getNameCount(); i++) {
    +            DirectoryStream<Path> dsOld = ds;
    +            try {
    +                ds = secure(ds).newDirectoryStream(toResolve.getName(i), LinkOption.NOFOLLOW_LINKS);
    +                dsOld.close();
    +            } catch (IOException ex) {
    +                ds.close();
    +                throw ex;
    +            }
    +        }
    +        return secure(ds);
    +    }
    +
    +    static SecureDirectoryStream<Path> secure(DirectoryStream<Path> ds) {
    +        if (ds instanceof SecureDirectoryStream) {
    +            return (SecureDirectoryStream<Path>) ds;
    +        }
    +        // do we want to bomb? do we want a different fallback option?
    +        throw new UnsupportedOperationException("FS Does not support secure directory streams.");
    +    }
    +
         default void removeFile(
                 SftpSubsystemProxy subsystem, Path path, boolean isDirectory)
                 throws IOException {
             Files.delete(path);
         }
    +
    +    default boolean noFollow(Collection<?> opts) {
    +        for (Object opt : opts) {
    +            if (LinkOption.NOFOLLOW_LINKS.equals(opt)) {
    +                return true;
    +            }
    +        }
    +        return false;
    +    }
     }
    
  • sshd-sftp/src/main/java/org/apache/sshd/sftp/server/SftpSubsystem.java+5 3 modified
    @@ -686,7 +686,7 @@ protected void doReadDir(Buffer buffer, int id) throws IOException {
                 // the upstream server decide.
                 if (!(file instanceof SftpPath)) {
                     LinkOption[] options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_READDIR, "", file);
    -                Boolean status = IoUtils.checkFileExists(file, options);
    +                Boolean status = IoUtils.checkFileExistsAnySymlinks(file, !IoUtils.followLinks(options));
                     if (status == null) {
                         throw new AccessDeniedException(file.toString(), file.toString(), "Cannot determine existence of read-dir");
                     }
    @@ -747,7 +747,7 @@ protected void doReadDir(Buffer buffer, int id) throws IOException {
         @Override
         protected String doOpenDir(int id, String path, Path dir, LinkOption... options) throws IOException {
             SftpPathImpl.withAttributeCache(dir, p -> {
    -            Boolean status = IoUtils.checkFileExists(p, options);
    +            Boolean status = IoUtils.checkFileExistsAnySymlinks(p, !IoUtils.followLinks(options));
                 if (status == null) {
                     throw signalOpenFailure(id, path, p, true,
                             new AccessDeniedException(p.toString(), p.toString(), "Cannot determine open-dir existence"));
    @@ -807,7 +807,9 @@ protected Map<String, Object> doFStat(int id, String handle, int flags) throws I
             Path file = fileHandle.getFile();
             LinkOption[] options = accessor.resolveFileAccessLinkOptions(
                     this, file, SftpConstants.SSH_FXP_FSTAT, "", true);
    -        return resolveFileAttributes(file, flags, options);
    +
    +        boolean followLinks = resolvePathResolutionFollowLinks(SftpConstants.SSH_FXP_FSTAT, handle, file);
    +        return resolveFileAttributes(file, flags, followLinks, options);
         }
     
         @Override
    
  • sshd-sftp/src/test/java/org/apache/sshd/sftp/client/SftpTest.java+4 3 modified
    @@ -383,7 +383,7 @@ private void testCannotEscapeRoot(boolean useAbsolutePath) throws Exception {
                 SftpClient.Attributes attrs = sftp.stat(escapePath);
                 fail("Unexpected escape success for path=" + escapePath + ": " + attrs);
             } catch (SftpException e) {
    -            int expected = OsUtils.isWin32() || (!useAbsolutePath)
    +            int expected = OsUtils.isWin32() && useAbsolutePath
                         ? SftpConstants.SSH_FX_INVALID_FILENAME
                         : SftpConstants.SSH_FX_NO_SUCH_FILE;
                 assertEquals("Mismatched status for " + escapePath,
    @@ -711,10 +711,11 @@ public SeekableByteChannel openFile(
     
                     @Override
                     public DirectoryStream<Path> openDirectory(
    -                        SftpSubsystemProxy subsystem, DirectoryHandle dirHandle, Path dir, String handle)
    +                        SftpSubsystemProxy subsystem, DirectoryHandle dirHandle, Path dir, String handle,
    +                        LinkOption... linkOptions)
                             throws IOException {
                         dirHolder.set(dir);
    -                    return SftpFileSystemAccessor.super.openDirectory(subsystem, dirHandle, dir, handle);
    +                    return SftpFileSystemAccessor.super.openDirectory(subsystem, dirHandle, dir, handle, linkOptions);
                     }
     
                     @Override
    
  • sshd-sftp/src/test/java/org/apache/sshd/sftp/client/SftpVersionsTest.java+8 4 modified
    @@ -291,9 +291,11 @@ public void testSftpACLEncodeDecode() throws Exception {
                 public Command createSubsystem(ChannelSession channel) throws IOException {
                     SftpSubsystem subsystem = new SftpSubsystem(channel, this) {
                         @Override
    -                    protected NavigableMap<String, Object> resolveFileAttributes(Path file, int flags, LinkOption... options)
    +                    protected NavigableMap<String, Object> resolveFileAttributes(
    +                            Path file, int flags, boolean neverFollowSymLinks, LinkOption... options)
                                 throws IOException {
    -                        NavigableMap<String, Object> attrs = super.resolveFileAttributes(file, flags, options);
    +                        NavigableMap<String, Object> attrs
    +                                = super.resolveFileAttributes(file, flags, neverFollowSymLinks, options);
                             if (MapEntryUtils.isEmpty(attrs)) {
                                 attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
                             }
    @@ -412,9 +414,11 @@ public void testSftpExtensionsEncodeDecode() throws Exception {
                 public Command createSubsystem(ChannelSession channel) throws IOException {
                     SftpSubsystem subsystem = new SftpSubsystem(channel, this) {
                         @Override
    -                    protected NavigableMap<String, Object> resolveFileAttributes(Path file, int flags, LinkOption... options)
    +                    protected NavigableMap<String, Object> resolveFileAttributes(
    +                            Path file, int flags, boolean neverFollowLinks, LinkOption... options)
                                 throws IOException {
    -                        NavigableMap<String, Object> attrs = super.resolveFileAttributes(file, flags, options);
    +                        NavigableMap<String, Object> attrs
    +                                = super.resolveFileAttributes(file, flags, neverFollowLinks, options);
                             if (MapEntryUtils.isEmpty(attrs)) {
                                 attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
                             }
    
10de190e7d3f

[SSHD-842] Split common utilities code from sshd-core into sshd-common (new artifact)

https://github.com/apache/mina-sshdGoldstein LyorSep 5, 2018via ghsa
300 files changed · +533 334
  • assembly/pom.xml+5 0 modified
    @@ -36,6 +36,11 @@
         </properties>
     
         <dependencies>
    +        <dependency>
    +            <groupId>org.apache.sshd</groupId>
    +            <artifactId>sshd-common</artifactId>
    +            <version>${project.version}</version>
    +        </dependency>
             <dependency>
                 <groupId>org.apache.sshd</groupId>
                 <artifactId>sshd-core</artifactId>
    
  • pom.xml+1 0 modified
    @@ -1135,6 +1135,7 @@
         </reporting>
     
         <modules>
    +        <module>sshd-common</module>
             <module>sshd-core</module>
             <module>sshd-mina</module>
             <module>sshd-netty</module>
    
  • sshd-cli/pom.xml+7 0 modified
    @@ -60,6 +60,13 @@
                 <version>${project.version}</version>
                 <type>test-jar</type>
                 <scope>test</scope>
    +        </dependency>
    +        <dependency>
    +            <groupId>org.apache.sshd</groupId>
    +            <artifactId>sshd-common</artifactId>
    +            <version>${project.version}</version>
    +            <type>test-jar</type>
    +            <scope>test</scope>
             </dependency>
                 <!-- For the I/O factories -->
             <dependency>
    
  • sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java+8 8 modified
    @@ -65,7 +65,7 @@
     import org.apache.sshd.common.compression.BuiltinCompressions;
     import org.apache.sshd.common.compression.Compression;
     import org.apache.sshd.common.config.CompressionConfigValue;
    -import org.apache.sshd.common.config.SshConfigFileReader;
    +import org.apache.sshd.common.config.ConfigFileReaderSupport;
     import org.apache.sshd.common.config.keys.BuiltinIdentities;
     import org.apache.sshd.common.config.keys.KeyUtils;
     import org.apache.sshd.common.config.keys.PublicKeyEntry;
    @@ -236,7 +236,7 @@ public static ClientSession setupClientSession(
                 }
     
                 if (port <= 0) {
    -                port = SshConfigFileReader.DEFAULT_PORT;
    +                port = ConfigFileReaderSupport.DEFAULT_PORT;
                 }
     
                 // TODO use a configurable wait time
    @@ -424,7 +424,7 @@ public static ServerKeyVerifier setupServerKeyVerifier(
             }
     
             String strictValue = Objects.toString(options.remove(KnownHostsServerKeyVerifier.STRICT_CHECKING_OPTION), "true");
    -        if (!SshConfigFileReader.parseBooleanValue(strictValue)) {
    +        if (!ConfigFileReaderSupport.parseBooleanValue(strictValue)) {
                 return current;
             }
     
    @@ -506,7 +506,7 @@ public static OutputStream resolveLoggingTargetStream(PrintStream stdout, PrintS
         }
     
         public static List<NamedFactory<Compression>> setupCompressions(PropertyResolver options, PrintStream stderr) {
    -        String argVal = PropertyResolverUtils.getString(options, SshConfigFileReader.COMPRESSION_PROP);
    +        String argVal = PropertyResolverUtils.getString(options, ConfigFileReaderSupport.COMPRESSION_PROP);
             if (GenericUtils.isEmpty(argVal)) {
                 return Collections.emptyList();
             }
    @@ -543,10 +543,10 @@ public static List<NamedFactory<Compression>> setupCompressions(
         }
     
         public static List<NamedFactory<Mac>> setupMacs(PropertyResolver options, PrintStream stderr) {
    -        String argVal = PropertyResolverUtils.getString(options, SshConfigFileReader.MACS_CONFIG_PROP);
    +        String argVal = PropertyResolverUtils.getString(options, ConfigFileReaderSupport.MACS_CONFIG_PROP);
             return GenericUtils.isEmpty(argVal)
                  ? Collections.emptyList()
    -             : setupMacs(SshConfigFileReader.MACS_CONFIG_PROP, argVal, null, stderr);
    +             : setupMacs(ConfigFileReaderSupport.MACS_CONFIG_PROP, argVal, null, stderr);
         }
     
         public static List<NamedFactory<Mac>> setupMacs(String argName, String argVal, List<NamedFactory<Mac>> current, PrintStream stderr) {
    @@ -571,10 +571,10 @@ public static List<NamedFactory<Mac>> setupMacs(String argName, String argVal, L
         }
     
         public static List<NamedFactory<Cipher>> setupCiphers(PropertyResolver options, PrintStream stderr) {
    -        String argVal = PropertyResolverUtils.getString(options, SshConfigFileReader.CIPHERS_CONFIG_PROP);
    +        String argVal = PropertyResolverUtils.getString(options, ConfigFileReaderSupport.CIPHERS_CONFIG_PROP);
             return GenericUtils.isEmpty(argVal)
                  ? Collections.emptyList()
    -             : setupCiphers(SshConfigFileReader.CIPHERS_CONFIG_PROP, argVal, null, stderr);
    +             : setupCiphers(ConfigFileReaderSupport.CIPHERS_CONFIG_PROP, argVal, null, stderr);
         }
     
         // returns null - e.g., re-specified or no supported cipher found
    
  • sshd-cli/src/main/java/org/apache/sshd/cli/client/SshKeyScanMain.java+2 2 modified
    @@ -63,7 +63,7 @@
     import org.apache.sshd.client.session.ClientSession;
     import org.apache.sshd.common.NamedFactory;
     import org.apache.sshd.common.cipher.ECCurves;
    -import org.apache.sshd.common.config.SshConfigFileReader;
    +import org.apache.sshd.common.config.ConfigFileReaderSupport;
     import org.apache.sshd.common.config.keys.BuiltinIdentities;
     import org.apache.sshd.common.config.keys.KeyUtils;
     import org.apache.sshd.common.config.keys.PublicKeyEntry;
    @@ -711,7 +711,7 @@ public static <S extends SshKeyScanMain> S setInputStream(S scanner, Collection<
         public static <S extends SshKeyScanMain> S initializeScanner(S scanner, Collection<String> hosts) throws IOException {
             setInputStream(scanner, hosts);
             if (scanner.getPort() <= 0) {
    -            scanner.setPort(SshConfigFileReader.DEFAULT_PORT);
    +            scanner.setPort(ConfigFileReaderSupport.DEFAULT_PORT);
             }
     
             if (scanner.getTimeout() <= 0L) {
    
  • sshd-cli/src/main/java/org/apache/sshd/cli/CliSupport.java+3 3 modified
    @@ -24,8 +24,8 @@
     import java.util.Map;
     import java.util.Objects;
     
    +import org.apache.sshd.common.config.ConfigFileReaderSupport;
     import org.apache.sshd.common.config.LogLevelValue;
    -import org.apache.sshd.common.config.SshConfigFileReader;
     import org.apache.sshd.common.helpers.AbstractFactoryManager;
     import org.apache.sshd.common.io.BuiltinIoServiceFactoryFactories;
     import org.apache.sshd.common.io.IoAcceptor;
    @@ -105,14 +105,14 @@ public static <M extends AbstractFactoryManager> M setupIoServiceFactory(
     
             manager.setIoServiceFactoryFactory(factory.create());
     
    -        String levelValue = (options == null) ? null : Objects.toString(options.get(SshConfigFileReader.LOG_LEVEL_CONFIG_PROP), null);
    +        String levelValue = (options == null) ? null : Objects.toString(options.get(ConfigFileReaderSupport.LOG_LEVEL_CONFIG_PROP), null);
             if (GenericUtils.isEmpty(levelValue)) {
                 return manager;
             }
     
             LogLevelValue level = LogLevelValue.fromName(levelValue);
             if (level == null) {
    -            throw new IllegalArgumentException("Unknown " + SshConfigFileReader.LOG_LEVEL_CONFIG_PROP + " option value: " + levelValue);
    +            throw new IllegalArgumentException("Unknown " + ConfigFileReaderSupport.LOG_LEVEL_CONFIG_PROP + " option value: " + levelValue);
             }
     
             if ((level != LogLevelValue.FATAL) && (level != LogLevelValue.ERROR) && (level != LogLevelValue.INFO)) {
    
  • sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerCliSupport.java+2 2 modified
    @@ -40,7 +40,7 @@
     import org.apache.sshd.common.NamedFactory;
     import org.apache.sshd.common.PropertyResolver;
     import org.apache.sshd.common.PropertyResolverUtils;
    -import org.apache.sshd.common.config.SshConfigFileReader;
    +import org.apache.sshd.common.config.ConfigFileReaderSupport;
     import org.apache.sshd.common.config.keys.BuiltinIdentities;
     import org.apache.sshd.common.config.keys.KeyUtils;
     import org.apache.sshd.common.keyprovider.KeyPairProvider;
    @@ -166,7 +166,7 @@ public static List<NamedFactory<Command>> resolveServerSubsystems(PrintStream st
                 return subsystems;
             }
     
    -        String nameList = (options == null) ? null : options.getString(SshConfigFileReader.SUBSYSTEM_CONFIG_PROP);
    +        String nameList = (options == null) ? null : options.getString(ConfigFileReaderSupport.SUBSYSTEM_CONFIG_PROP);
             if ("none".equalsIgnoreCase(nameList)) {
                 return Collections.emptyList();
             }
    
  • sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java+3 2 modified
    @@ -30,6 +30,7 @@
     import org.apache.sshd.common.NamedResource;
     import org.apache.sshd.common.PropertyResolver;
     import org.apache.sshd.common.PropertyResolverUtils;
    +import org.apache.sshd.common.config.ConfigFileReaderSupport;
     import org.apache.sshd.common.config.SshConfigFileReader;
     import org.apache.sshd.common.keyprovider.KeyPairProvider;
     import org.apache.sshd.common.util.GenericUtils;
    @@ -138,7 +139,7 @@ public static void main(String[] args) throws Exception {
                             keyFiles = new LinkedList<>();
                         }
                         keyFiles.add(optValue);
    -                } else if (SshConfigFileReader.PORT_CONFIG_PROP.equals(optName)) {
    +                } else if (ConfigFileReaderSupport.PORT_CONFIG_PROP.equals(optName)) {
                         port = Integer.parseInt(optValue);
                     } else {
                         options.put(optName, optValue);
    @@ -166,7 +167,7 @@ public static void main(String[] args) throws Exception {
             setupServerBanner(sshd, resolver);
             sshd.setPort(port);
     
    -        String macsOverride = resolver.getString(SshConfigFileReader.MACS_CONFIG_PROP);
    +        String macsOverride = resolver.getString(ConfigFileReaderSupport.MACS_CONFIG_PROP);
             if (GenericUtils.isNotEmpty(macsOverride)) {
                 SshConfigFileReader.configureMacs(sshd, macsOverride, true, true);
             }
    
  • sshd-cli/src/test/java/org/apache/sshd/cli/server/SshFsMounter.java+6 5 modified
    @@ -35,7 +35,7 @@
     
     import org.apache.sshd.common.PropertyResolver;
     import org.apache.sshd.common.PropertyResolverUtils;
    -import org.apache.sshd.common.config.SshConfigFileReader;
    +import org.apache.sshd.common.config.ConfigFileReaderSupport;
     import org.apache.sshd.common.io.BuiltinIoServiceFactoryFactories;
     import org.apache.sshd.common.io.IoServiceFactory;
     import org.apache.sshd.common.util.GenericUtils;
    @@ -56,7 +56,8 @@
     import org.apache.sshd.server.session.ServerSession;
     import org.apache.sshd.server.shell.ShellFactory;
     import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
    -import org.apache.sshd.util.test.Utils;
    +import org.apache.sshd.util.test.CommonTestSupportUtils;
    +import org.apache.sshd.util.test.CoreTestSupportUtils;
     
     /**
      * A basic implementation to allow remote mounting of the local file system via SFTP
    @@ -249,7 +250,7 @@ private SshFsMounter() {
         //////////////////////////////////////////////////////////////////////////
     
         public static void main(String[] args) throws Exception {
    -        int port = SshConfigFileReader.DEFAULT_PORT;
    +        int port = ConfigFileReaderSupport.DEFAULT_PORT;
             boolean error = false;
             Map<String, Object> options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
             int numArgs = GenericUtils.length(args);
    @@ -296,7 +297,7 @@ public static void main(String[] args) throws Exception {
             }
     
             SshServer sshd = error ? null : setupIoServiceFactory(
    -            Utils.setupTestServer(SshFsMounter.class), options, System.out, System.err, args);
    +            CoreTestSupportUtils.setupTestServer(SshFsMounter.class), options, System.out, System.err, args);
             if (sshd == null) {
                 error = true;
             }
    @@ -309,7 +310,7 @@ public static void main(String[] args) throws Exception {
             Map<String, Object> props = sshd.getProperties();
             props.putAll(options);
             PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(options);
    -        File targetFolder = Objects.requireNonNull(Utils.detectTargetFolder(MounterCommandFactory.class), "Failed to detect target folder");
    +        File targetFolder = Objects.requireNonNull(CommonTestSupportUtils.detectTargetFolder(MounterCommandFactory.class), "Failed to detect target folder");
             if (SecurityUtils.isBouncyCastleRegistered()) {
                 sshd.setKeyPairProvider(SecurityUtils.createGeneratorHostKeyProvider(new File(targetFolder, "key.pem").toPath()));
             } else {
    
  • sshd-common/pom.xml+128 0 added
    @@ -0,0 +1,128 @@
    +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    +
    +
    +    <!--
    +
    +        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.
    +    -->
    +
    +    <modelVersion>4.0.0</modelVersion>
    +
    +    <parent>
    +        <groupId>org.apache.sshd</groupId>
    +        <artifactId>sshd</artifactId>
    +        <version>2.0.1-SNAPSHOT</version>
    +        <relativePath>..</relativePath>
    +    </parent>
    +
    +    <artifactId>sshd-common</artifactId>
    +    <name>Apache Mina SSHD :: Common support utilities</name>
    +    <packaging>jar</packaging>
    +    <inceptionYear>2018</inceptionYear>
    +
    +    <properties>
    +        <projectRoot>${project.basedir}/..</projectRoot>
    +    </properties>
    +
    +    <dependencies>
    +        <dependency>
    +            <groupId>org.slf4j</groupId>
    +            <artifactId>slf4j-api</artifactId>
    +        </dependency>
    +
    +        <dependency>
    +            <groupId>org.bouncycastle</groupId>
    +            <artifactId>bcpg-jdk15on</artifactId>
    +            <optional>true</optional>
    +        </dependency>
    +        <dependency>
    +            <groupId>org.bouncycastle</groupId>
    +            <artifactId>bcpkix-jdk15on</artifactId>
    +            <optional>true</optional>
    +        </dependency>
    +
    +            <!-- For ed25519 support -->
    +        <dependency>
    +            <groupId>net.i2p.crypto</groupId>
    +            <artifactId>eddsa</artifactId>
    +            <optional>true</optional>
    +        </dependency>
    +
    +            <!-- test dependencies -->
    +        <dependency>
    +            <groupId>org.slf4j</groupId>
    +            <artifactId>jcl-over-slf4j</artifactId>
    +            <scope>test</scope>
    +        </dependency>
    +        <dependency>
    +            <groupId>org.slf4j</groupId>
    +            <artifactId>slf4j-log4j12</artifactId>
    +            <scope>test</scope>
    +        </dependency>
    +        <dependency>
    +            <groupId>junit</groupId>
    +            <artifactId>junit</artifactId>
    +            <scope>test</scope>
    +        </dependency>
    +        <dependency>
    +            <groupId>org.mockito</groupId>
    +            <artifactId>mockito-core</artifactId>
    +            <scope>test</scope>
    +        </dependency>
    +        <dependency>
    +            <groupId>org.apache.servicemix.bundles</groupId>
    +            <artifactId>org.apache.servicemix.bundles.not-yet-commons-ssl</artifactId>
    +            <scope>test</scope>
    +        </dependency>
    +    </dependencies>
    +
    +    <build>
    +        <resources>
    +            <resource>
    +                <directory>src/main/filtered-resources</directory>
    +                <filtering>true</filtering>
    +            </resource>
    +        </resources>
    +        <plugins>
    +            <!-- publish the test-jar since it contains some classes used by other
    +                artifacts tests -->
    +            <plugin>
    +                <groupId>org.apache.maven.plugins</groupId>
    +                <artifactId>maven-jar-plugin</artifactId>
    +                <executions>
    +                    <execution>
    +                        <goals>
    +                            <goal>test-jar</goal>
    +                        </goals>
    +                        <configuration>
    +                            <includes>
    +                                <include>org/apache/sshd/util/test/**/*</include>
    +                            </includes>
    +                        </configuration>
    +                    </execution>
    +                </executions>
    +            </plugin>
    +            <plugin>
    +                <groupId>org.apache.maven.plugins</groupId>
    +                <artifactId>maven-surefire-plugin</artifactId>
    +                <configuration>
    +                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
    +                    <reportsDirectory>${project.build.directory}/surefire-reports-common</reportsDirectory>
    +                </configuration>
    +            </plugin>
    +        </plugins>
    +    </build>
    +</project>
    
  • sshd-common/src/main/filtered-resources/org/apache/sshd/sshd-version.properties+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/auth/AuthenticationIdentitiesProvider.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/auth/hostbased/HostKeyIdentityProvider.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/auth/password/PasswordIdentityProvider.java+2 16 renamed
    @@ -25,7 +25,6 @@
     import java.util.function.Function;
     import java.util.function.Supplier;
     
    -import org.apache.sshd.client.session.ClientSession;
     import org.apache.sshd.common.util.GenericUtils;
     
     /**
    @@ -62,20 +61,6 @@ public String toString() {
          */
         Iterable<String> loadPasswords();
     
    -    /**
    -     * Creates a &quot;unified&quot; {@link Iterator} of passwords out of the registered
    -     * passwords and the extra available ones as a single iterator of passwords
    -     *
    -     * @param session The {@link ClientSession} - ignored if {@code null} (i.e., empty
    -     * iterator returned)
    -     * @return The wrapping iterator
    -     * @see ClientSession#getRegisteredIdentities()
    -     * @see ClientSession#getPasswordIdentityProvider()
    -     */
    -    static Iterator<String> iteratorOf(ClientSession session) {
    -        return (session == null) ? Collections.<String>emptyIterator() : iteratorOf(session.getRegisteredIdentities(), session.getPasswordIdentityProvider());
    -    }
    -
         /**
          * Creates a &quot;unified&quot; {@link Iterator} of passwords out of 2 possible
          * {@link PasswordIdentityProvider}
    @@ -114,7 +99,8 @@ static Iterator<String> iteratorOf(PasswordIdentityProvider provider) {
          * @return The resolved provider
          * @see #multiProvider(PasswordIdentityProvider...)
          */
    -    static PasswordIdentityProvider resolvePasswordIdentityProvider(PasswordIdentityProvider identities, PasswordIdentityProvider passwords) {
    +    static PasswordIdentityProvider resolvePasswordIdentityProvider(
    +            PasswordIdentityProvider identities, PasswordIdentityProvider passwords) {
             if ((passwords == null) || (identities == passwords)) {
                 return identities;
             } else if (identities == null) {
    
  • sshd-common/src/main/java/org/apache/sshd/client/auth/pubkey/PublicKeyIdentity.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java+2 2 renamed
    @@ -47,8 +47,8 @@ public class DefaultConfigFileHostEntryResolver extends ConfigFileHostEntryResol
     
         /**
          * @param strict If {@code true} then makes sure that the containing folder
    -     *               has 0700 access and the file 0644. <B>Note:</B> for <I>Windows</I> it
    -     *               does not check these permissions
    +     * has 0700 access and the file 0644. <B>Note:</B> for <I>Windows</I> it
    +     * does not check these permissions
          * @see #validateStrictConfigFilePermissions(Path, LinkOption...)
          */
         public DefaultConfigFileHostEntryResolver(boolean strict) {
    
  • sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java+7 7 renamed
    @@ -48,7 +48,7 @@
     import java.util.TreeMap;
     
     import org.apache.sshd.common.auth.MutableUserHolder;
    -import org.apache.sshd.common.config.SshConfigFileReader;
    +import org.apache.sshd.common.config.ConfigFileReaderSupport;
     import org.apache.sshd.common.config.keys.IdentityUtils;
     import org.apache.sshd.common.config.keys.PublicKeyEntry;
     import org.apache.sshd.common.util.GenericUtils;
    @@ -73,7 +73,7 @@ public class HostConfigEntry extends HostPatternsHolder implements MutableUserHo
     
         public static final String HOST_CONFIG_PROP = "Host";
         public static final String HOST_NAME_CONFIG_PROP = "HostName";
    -    public static final String PORT_CONFIG_PROP = SshConfigFileReader.PORT_CONFIG_PROP;
    +    public static final String PORT_CONFIG_PROP = ConfigFileReaderSupport.PORT_CONFIG_PROP;
         public static final String USER_CONFIG_PROP = "User";
         public static final String IDENTITY_FILE_CONFIG_PROP = "IdentityFile";
         /**
    @@ -487,8 +487,8 @@ public void processProperty(String name, Collection<String> valsList, boolean ig
                 }
             } else if (EXCLUSIVE_IDENTITIES_CONFIG_PROP.equalsIgnoreCase(key)) {
                 setIdentitiesOnly(
    -                    SshConfigFileReader.parseBooleanValue(
    -                            ValidateUtils.checkNotNullAndNotEmpty(joinedValue, "No identities option value")));
    +                ConfigFileReaderSupport.parseBooleanValue(
    +                    ValidateUtils.checkNotNullAndNotEmpty(joinedValue, "No identities option value")));
             }
         }
     
    @@ -565,7 +565,7 @@ public <A extends Appendable> A append(A sb) throws IOException {
             appendNonEmptyProperty(sb, USER_CONFIG_PROP, getUsername());
             appendNonEmptyValues(sb, IDENTITY_FILE_CONFIG_PROP, getIdentities());
             if (exclusiveIdentites != null) {
    -            appendNonEmptyProperty(sb, EXCLUSIVE_IDENTITIES_CONFIG_PROP, SshConfigFileReader.yesNoValueOf(exclusiveIdentites));
    +            appendNonEmptyProperty(sb, EXCLUSIVE_IDENTITIES_CONFIG_PROP, ConfigFileReaderSupport.yesNoValueOf(exclusiveIdentites));
             }
             appendNonEmptyProperties(sb, getProperties());
             return sb;
    @@ -673,7 +673,7 @@ public static <A extends Appendable> A appendNonEmptyValues(A sb, String name, C
          * @param entries The entries - ignored if {@code null}/empty
          * @return A {@link HostConfigEntryResolver} wrapper using the entries
          */
    -    public static HostConfigEntryResolver toHostConfigEntryResolver(final Collection<? extends HostConfigEntry> entries) {
    +    public static HostConfigEntryResolver toHostConfigEntryResolver(Collection<? extends HostConfigEntry> entries) {
             if (GenericUtils.isEmpty(entries)) {
                 return HostConfigEntryResolver.EMPTY;
             } else {
    @@ -838,7 +838,7 @@ public static List<HostConfigEntry> readHostConfigEntries(BufferedReader rdr) th
                     continue;
                 }
     
    -            int pos = line.indexOf(SshConfigFileReader.COMMENT_CHAR);
    +            int pos = line.indexOf(ConfigFileReaderSupport.COMMENT_CHAR);
                 if (pos == 0) {
                     continue;
                 }
    
  • sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostPatternValue.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java+2 2 renamed
    @@ -36,7 +36,7 @@
     import java.util.Collections;
     import java.util.List;
     
    -import org.apache.sshd.common.config.SshConfigFileReader;
    +import org.apache.sshd.common.config.ConfigFileReaderSupport;
     import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
     import org.apache.sshd.common.config.keys.PublicKeyEntry;
     import org.apache.sshd.common.util.GenericUtils;
    @@ -196,7 +196,7 @@ public static List<KnownHostEntry> readKnownHostEntries(BufferedReader rdr) thro
                     continue;
                 }
     
    -            int pos = line.indexOf(SshConfigFileReader.COMMENT_CHAR);
    +            int pos = line.indexOf(ConfigFileReaderSupport.COMMENT_CHAR);
                 if (pos == 0) {
                     continue;
                 }
    
  • sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/config/keys/BuiltinClientIdentitiesWatcher.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentitiesWatcher.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityFileWatcher.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java+0 57 renamed
    @@ -31,7 +31,6 @@
     import java.util.TreeMap;
     import java.util.function.Function;
     
    -import org.apache.sshd.client.SshClient;
     import org.apache.sshd.common.NamedResource;
     import org.apache.sshd.common.config.keys.BuiltinIdentities;
     import org.apache.sshd.common.config.keys.FilePasswordProvider;
    @@ -95,62 +94,6 @@ public static String getIdentityFileName(String type) {
             return IdentityUtils.getIdentityFileName(ID_FILE_PREFIX, type, ID_FILE_SUFFIX);
         }
     
    -    /**
    -     * @param <C>           The generic client class
    -     * @param client        The {@link SshClient} to updated
    -     * @param strict        If {@code true} then files that do not have the required
    -     *                      access rights are excluded from consideration
    -     * @param supportedOnly If {@code true} then ignore identities that are not
    -     *                      supported internally
    -     * @param provider      A {@link FilePasswordProvider} - may be {@code null}
    -     *                      if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
    -     *                      to {@link FilePasswordProvider#getPassword(String)} is the path of the
    -     *                      file whose key is to be loaded
    -     * @param options       The {@link LinkOption}s to apply when checking
    -     *                      for existence
    -     * @return The updated <tt>client</tt> instance - provided a non-{@code null}
    -     * {@link KeyPairProvider} was generated
    -     * @throws IOException              If failed to access the file system
    -     * @throws GeneralSecurityException If failed to load the keys
    -     * @see #setKeyPairProvider(SshClient, Path, boolean, boolean, FilePasswordProvider, LinkOption...)
    -     */
    -    public static <C extends SshClient> C setKeyPairProvider(
    -            C client, boolean strict, boolean supportedOnly, FilePasswordProvider provider, LinkOption... options)
    -            throws IOException, GeneralSecurityException {
    -        return setKeyPairProvider(client, PublicKeyEntry.getDefaultKeysFolderPath(), strict, supportedOnly, provider, options);
    -    }
    -
    -    /**
    -     * @param <C>           The generic client class
    -     * @param client        The {@link SshClient} to updated
    -     * @param dir           The folder to scan for the built-in identities
    -     * @param strict        If {@code true} then files that do not have the required
    -     *                      access rights are excluded from consideration
    -     * @param supportedOnly If {@code true} then ignore identities that are not
    -     *                      supported internally
    -     * @param provider      A {@link FilePasswordProvider} - may be {@code null}
    -     *                      if the loaded keys are <U>guaranteed</U> not to be encrypted. The argument
    -     *                      to {@link FilePasswordProvider#getPassword(String)} is the path of the
    -     *                      file whose key is to be loaded
    -     * @param options       The {@link LinkOption}s to apply when checking
    -     *                      for existence
    -     * @return The updated <tt>client</tt> instance - provided a non-{@code null}
    -     * {@link KeyPairProvider} was generated
    -     * @throws IOException              If failed to access the file system
    -     * @throws GeneralSecurityException If failed to load the keys
    -     * @see #loadDefaultKeyPairProvider(Path, boolean, boolean, FilePasswordProvider, LinkOption...)
    -     */
    -    public static <C extends SshClient> C setKeyPairProvider(
    -            C client, Path dir, boolean strict, boolean supportedOnly, FilePasswordProvider provider, LinkOption... options)
    -            throws IOException, GeneralSecurityException {
    -        KeyPairProvider kpp = loadDefaultKeyPairProvider(dir, strict, supportedOnly, provider, options);
    -        if (kpp != null) {
    -            client.setKeyPairProvider(kpp);
    -        }
    -
    -        return client;
    -    }
    -
         /**
          * @param strict        If {@code true} then files that do not have the required
          *                      access rights are excluded from consideration
    
  • sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityProvider.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/client/config/keys/DefaultClientIdentitiesWatcher.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/AttributeStore.java+1 56 renamed
    @@ -19,11 +19,6 @@
     
     package org.apache.sshd.common;
     
    -import java.util.Objects;
    -
    -import org.apache.sshd.common.channel.Channel;
    -import org.apache.sshd.common.session.Session;
    -
     /**
      * Provides the capability to attach in-memory attributes to the entity
      *
    @@ -98,55 +93,5 @@ public AttributeKey() {
          * @return {@code null} if there is no value associated with the specified key
          */
         <T> T resolveAttribute(AttributeKey<T> key);
    -
    -    /**
    -     * @param <T> The generic attribute type
    -     * @param manager The {@link FactoryManager} - ignored if {@code null}
    -     * @param key The attribute key - never {@code null}
    -     * @return Associated value - {@code null} if not found
    -     */
    -    static <T> T resolveAttribute(FactoryManager manager, AttributeKey<T> key) {
    -        Objects.requireNonNull(key, "No key");
    -        return (manager == null) ? null : manager.getAttribute(key);
    -    }
    -
    -    /**
    -     * Attempts to use the session's attribute, if not found then tries the factory manager
    -     *
    -     * @param <T> The generic attribute type
    -     * @param session The {@link Session} - ignored if {@code null}
    -     * @param key The attribute key - never {@code null}
    -     * @return Associated value - {@code null} if not found
    -     * @see Session#getFactoryManager()
    -     * @see #resolveAttribute(FactoryManager, AttributeKey)
    -     */
    -    static <T> T resolveAttribute(Session session, AttributeKey<T> key) {
    -        Objects.requireNonNull(key, "No key");
    -        if (session == null) {
    -            return null;
    -        }
    -
    -        T value = session.getAttribute(key);
    -        return (value != null) ? value : resolveAttribute(session.getFactoryManager(), key);
    -    }
    -
    -    /**
    -     * Attempts to use the channel attribute, if not found then tries the session
    -     *
    -     * @param <T> The generic attribute type
    -     * @param channel The {@link Channel} - ignored if {@code null}
    -     * @param key The attribute key - never {@code null}
    -     * @return Associated value - {@code null} if not found
    -     * @see Session#getFactoryManager()
    -     * @see #resolveAttribute(Session, AttributeKey)
    -     */
    -    static <T> T resolveAttribute(Channel channel, AttributeKey<T> key) {
    -        Objects.requireNonNull(key, "No key");
    -        if (channel == null) {
    -            return null;
    -        }
    -
    -        T value = channel.getAttribute(key);
    -        return (value != null) ? value : resolveAttribute(channel.getSession(), key);
    -    }
     }
    +
    
  • sshd-common/src/main/java/org/apache/sshd/common/auth/MutableUserHolder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/auth/UsernameHolder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/BuiltinFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseCipher.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseRC4Cipher.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/cipher/BuiltinCiphers.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/cipher/CipherFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/cipher/CipherInformation.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/cipher/Cipher.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/cipher/CipherNone.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/cipher/ECCurves.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/cipher/package.html+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/Closeable.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/compression/BaseCompression.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/compression/BuiltinCompressions.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/compression/CompressionDelayedZlib.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/compression/CompressionFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/compression/CompressionInformation.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/compression/Compression.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/compression/CompressionNone.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/compression/CompressionZlib.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/compression/package.html+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/CompressionConfigValue.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/ConfigFileReaderSupport.java+230 0 added
    @@ -0,0 +1,230 @@
    +/*
    + * 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.common.config;
    +
    +import java.io.BufferedReader;
    +import java.io.File;
    +import java.io.FileInputStream;
    +import java.io.IOException;
    +import java.io.InputStream;
    +import java.io.InputStreamReader;
    +import java.io.Reader;
    +import java.io.StreamCorruptedException;
    +import java.net.URL;
    +import java.nio.charset.StandardCharsets;
    +import java.nio.file.Files;
    +import java.nio.file.OpenOption;
    +import java.nio.file.Path;
    +import java.util.Properties;
    +import java.util.concurrent.TimeUnit;
    +
    +import org.apache.sshd.common.keyprovider.KeyPairProvider;
    +import org.apache.sshd.common.util.GenericUtils;
    +import org.apache.sshd.common.util.io.IoUtils;
    +import org.apache.sshd.common.util.io.NoCloseInputStream;
    +import org.apache.sshd.common.util.io.NoCloseReader;
    +import org.apache.sshd.common.util.net.SshdSocketAddress;
    +
    +/**
    + * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
    + * @see <a href="https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5">ssh_config(5)</a>
    + */
    +public final class ConfigFileReaderSupport {
    +
    +    public static final char COMMENT_CHAR = '#';
    +
    +    public static final String COMPRESSION_PROP = "Compression";
    +    public static final String DEFAULT_COMPRESSION = CompressionConfigValue.NO.getName();
    +    public static final String MAX_SESSIONS_CONFIG_PROP = "MaxSessions";
    +    public static final int DEFAULT_MAX_SESSIONS = 10;
    +    public static final String PASSWORD_AUTH_CONFIG_PROP = "PasswordAuthentication";
    +    public static final String DEFAULT_PASSWORD_AUTH = "no";
    +    public static final boolean DEFAULT_PASSWORD_AUTH_VALUE = parseBooleanValue(DEFAULT_PASSWORD_AUTH);
    +    public static final String LISTEN_ADDRESS_CONFIG_PROP = "ListenAddress";
    +    public static final String DEFAULT_BIND_ADDRESS = SshdSocketAddress.IPV4_ANYADDR;
    +    public static final String PORT_CONFIG_PROP = "Port";
    +    public static final int DEFAULT_PORT = 22;
    +    public static final String KEEP_ALIVE_CONFIG_PROP = "TCPKeepAlive";
    +    public static final boolean DEFAULT_KEEP_ALIVE = true;
    +    public static final String USE_DNS_CONFIG_PROP = "UseDNS";
    +    // NOTE: the usual default is TRUE
    +    public static final boolean DEFAULT_USE_DNS = true;
    +    public static final String PUBKEY_AUTH_CONFIG_PROP = "PubkeyAuthentication";
    +    public static final String DEFAULT_PUBKEY_AUTH = "yes";
    +    public static final boolean DEFAULT_PUBKEY_AUTH_VALUE = parseBooleanValue(DEFAULT_PUBKEY_AUTH);
    +    public static final String AUTH_KEYS_FILE_CONFIG_PROP = "AuthorizedKeysFile";
    +    public static final String MAX_AUTH_TRIES_CONFIG_PROP = "MaxAuthTries";
    +    public static final int DEFAULT_MAX_AUTH_TRIES = 6;
    +    public static final String MAX_STARTUPS_CONFIG_PROP = "MaxStartups";
    +    public static final int DEFAULT_MAX_STARTUPS = 10;
    +    public static final String LOGIN_GRACE_TIME_CONFIG_PROP = "LoginGraceTime";
    +    public static final long DEFAULT_LOGIN_GRACE_TIME = TimeUnit.SECONDS.toMillis(120);
    +    public static final String KEY_REGENERATE_INTERVAL_CONFIG_PROP = "KeyRegenerationInterval";
    +    public static final long DEFAULT_REKEY_TIME_LIMIT = TimeUnit.HOURS.toMillis(1L);
    +    // see http://manpages.ubuntu.com/manpages/precise/en/man5/sshd_config.5.html
    +    public static final String CIPHERS_CONFIG_PROP = "Ciphers";
    +    public static final String DEFAULT_CIPHERS =
    +            "aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,aes192-cbc,aes256-cbc,arcfour";
    +    // see http://manpages.ubuntu.com/manpages/precise/en/man5/sshd_config.5.html
    +    public static final String MACS_CONFIG_PROP = "MACs";
    +    public static final String DEFAULT_MACS =
    +            "hmac-md5,hmac-sha1,umac-64@openssh.com,hmac-ripemd160,hmac-sha1-96,hmac-md5-96,hmac-sha2-256,hmac-sha2-256-96,hmac-sha2-512,hmac-sha2-512-96";
    +    // see http://manpages.ubuntu.com/manpages/precise/en/man5/sshd_config.5.html
    +    public static final String KEX_ALGORITHMS_CONFIG_PROP = "KexAlgorithms";
    +    public static final String DEFAULT_KEX_ALGORITHMS =
    +            "ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521"
    +                    + "," + "diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1"
    +                    // RFC-8268 groups
    +                    + "," + "diffie-hellman-group18-sha512,diffie-hellman-group17-sha512"
    +                    + "," + "diffie-hellman-group16-sha512,diffie-hellman-group15-sha512"
    +                    + "," + "diffie-hellman-group14-sha256"
    +                    // Legacy groups
    +                    + "," + "diffie-hellman-group14-sha1,diffie-hellman-group1-sha1";
    +    // see http://linux.die.net/man/5/ssh_config
    +    public static final String HOST_KEY_ALGORITHMS_CONFIG_PROP = "HostKeyAlgorithms";
    +    // see https://tools.ietf.org/html/rfc5656
    +    public static final String DEFAULT_HOST_KEY_ALGORITHMS =
    +            KeyPairProvider.SSH_RSA + "," + KeyPairProvider.SSH_DSS;
    +    // see http://manpages.ubuntu.com/manpages/precise/en/man5/sshd_config.5.html
    +    public static final String LOG_LEVEL_CONFIG_PROP = "LogLevel";
    +    public static final LogLevelValue DEFAULT_LOG_LEVEL = LogLevelValue.INFO;
    +    // see https://www.freebsd.org/cgi/man.cgi?query=sshd_config&sektion=5
    +    public static final String SYSLOG_FACILITY_CONFIG_PROP = "SyslogFacility";
    +    public static final SyslogFacilityValue DEFAULT_SYSLOG_FACILITY = SyslogFacilityValue.AUTH;
    +    public static final String SUBSYSTEM_CONFIG_PROP = "Subsystem";
    +
    +    private ConfigFileReaderSupport() {
    +        throw new UnsupportedOperationException("No instance");
    +    }
    +
    +    public static Properties readConfigFile(File file) throws IOException {
    +        return readConfigFile(file.toPath(), IoUtils.EMPTY_OPEN_OPTIONS);
    +    }
    +
    +    public static Properties readConfigFile(Path path, OpenOption... options) throws IOException {
    +        try (InputStream input = Files.newInputStream(path, options)) {
    +            return readConfigFile(input, true);
    +        }
    +    }
    +
    +    public static Properties readConfigFile(URL url) throws IOException {
    +        try (InputStream input = url.openStream()) {
    +            return readConfigFile(input, true);
    +        }
    +    }
    +
    +    public static Properties readConfigFile(String path) throws IOException {
    +        try (InputStream input = new FileInputStream(path)) {
    +            return readConfigFile(input, true);
    +        }
    +    }
    +
    +    public static Properties readConfigFile(InputStream input, boolean okToClose) throws IOException {
    +        try (Reader reader = new InputStreamReader(NoCloseInputStream.resolveInputStream(input, okToClose), StandardCharsets.UTF_8)) {
    +            return readConfigFile(reader, true);
    +        }
    +    }
    +
    +    public static Properties readConfigFile(Reader reader, boolean okToClose) throws IOException {
    +        try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(reader, okToClose))) {
    +            return readConfigFile(buf);
    +        }
    +    }
    +
    +    /**
    +     * Reads the configuration file contents into a {@link Properties} instance.
    +     * <B>Note:</B> multiple keys value are concatenated using a comma - it is up to
    +     * the caller to know which keys are expected to have multiple values and handle
    +     * the split accordingly
    +     *
    +     * @param rdr The {@link BufferedReader} for reading the file
    +     * @return The read properties
    +     * @throws IOException If failed to read or malformed content
    +     */
    +    public static Properties readConfigFile(BufferedReader rdr) throws IOException {
    +        Properties props = new Properties();
    +        int lineNumber = 1;
    +        for (String line = rdr.readLine(); line != null; line = rdr.readLine(), lineNumber++) {
    +            line = GenericUtils.replaceWhitespaceAndTrim(line);
    +            if (GenericUtils.isEmpty(line)) {
    +                continue;
    +            }
    +
    +            int pos = line.indexOf(COMMENT_CHAR);
    +            if (pos == 0) {
    +                continue;
    +            }
    +
    +            if (pos > 0) {
    +                line = line.substring(0, pos);
    +                line = line.trim();
    +            }
    +
    +            /*
    +             * Some options use '=', others use ' ' - try both
    +             * NOTE: we do not validate the format for each option separately
    +             */
    +            pos = line.indexOf(' ');
    +            if (pos < 0) {
    +                pos = line.indexOf('=');
    +            }
    +
    +            if (pos < 0) {
    +                throw new StreamCorruptedException("No delimiter at line " + lineNumber + ": " + line);
    +            }
    +
    +            String key = line.substring(0, pos);
    +            String value = line.substring(pos + 1).trim();
    +            // see if need to concatenate multi-valued keys
    +            String prev = props.getProperty(key);
    +            if (!GenericUtils.isEmpty(prev)) {
    +                value = prev + "," + value;
    +            }
    +
    +            props.setProperty(key, value);
    +        }
    +
    +        return props;
    +    }
    +
    +    /**
    +     * @param v Checks if the value is &quot;yes&quot;, &quot;y&quot;
    +     *          or &quot;on&quot; or &quot;true&quot;.
    +     * @return The result - <B>Note:</B> {@code null}/empty values are
    +     * interpreted as {@code false}
    +     */
    +    public static boolean parseBooleanValue(String v) {
    +        return "yes".equalsIgnoreCase(v)
    +            || "y".equalsIgnoreCase(v)
    +            || "on".equalsIgnoreCase(v)
    +            || "true".equalsIgnoreCase(v);
    +    }
    +
    +    /**
    +     * Returns a &quot;yes&quot; or &quot;no&quot; value based on the input
    +     * parameter
    +     *
    +     * @param flag The required state
    +     * @return &quot;yes&quot; if {@code true}, &quot;no&quot; otherwise
    +     */
    +    public static String yesNoValueOf(boolean flag) {
    +        return flag ? "yes" : "no";
    +    }
    +}
    
  • sshd-common/src/main/java/org/apache/sshd/common/config/FactoriesListParseResult.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java+3 15 renamed
    @@ -47,9 +47,6 @@
     import org.apache.sshd.common.util.ValidateUtils;
     import org.apache.sshd.common.util.io.NoCloseInputStream;
     import org.apache.sshd.common.util.io.NoCloseReader;
    -import org.apache.sshd.server.auth.pubkey.KeySetPublickeyAuthenticator;
    -import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
    -import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator;
     
     /**
      * Represents an entry in the user's {@code authorized_keys} file according
    @@ -148,18 +145,9 @@ public String toString() {
                     + (GenericUtils.isEmpty(kc) ? "" : " " + kc);
         }
     
    -    public static PublickeyAuthenticator fromAuthorizedEntries(PublicKeyEntryResolver fallbackResolver, Collection<? extends AuthorizedKeyEntry> entries)
    -            throws IOException, GeneralSecurityException {
    -        Collection<PublicKey> keys = resolveAuthorizedKeys(fallbackResolver, entries);
    -        if (GenericUtils.isEmpty(keys)) {
    -            return RejectAllPublickeyAuthenticator.INSTANCE;
    -        } else {
    -            return new KeySetPublickeyAuthenticator(keys);
    -        }
    -    }
    -
    -    public static List<PublicKey> resolveAuthorizedKeys(PublicKeyEntryResolver fallbackResolver, Collection<? extends AuthorizedKeyEntry> entries)
    -            throws IOException, GeneralSecurityException {
    +    public static List<PublicKey> resolveAuthorizedKeys(
    +            PublicKeyEntryResolver fallbackResolver, Collection<? extends AuthorizedKeyEntry> entries)
    +                    throws IOException, GeneralSecurityException {
             if (GenericUtils.isEmpty(entries)) {
                 return Collections.emptyList();
             }
    
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/BuiltinIdentities.java+4 4 renamed
    @@ -58,12 +58,12 @@ public boolean isSupported() {
         };
     
         public static final Set<BuiltinIdentities> VALUES =
    -            Collections.unmodifiableSet(EnumSet.allOf(BuiltinIdentities.class));
    +        Collections.unmodifiableSet(EnumSet.allOf(BuiltinIdentities.class));
     
         public static final Set<String> NAMES =
    -            Collections.unmodifiableSet(
    -                    GenericUtils.asSortedSet(
    -                            String.CASE_INSENSITIVE_ORDER, NamedResource.getNameList(VALUES)));
    +        Collections.unmodifiableSet(
    +            GenericUtils.asSortedSet(
    +                String.CASE_INSENSITIVE_ORDER, NamedResource.getNameList(VALUES)));
     
         private final String name;
         private final String algorithm;
    
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/Identity.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityResourceLoader.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/IdentityUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractIdentityResourceLoader.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractKeyEntryResolver.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractPrivateKeyEntryDecoder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/AbstractPublicKeyEntryDecoder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/DSSPublicKeyEntryDecoder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/RSAPublicKeyDecoder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyEntryResolver.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyRandomArt.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java+3 3 renamed
    @@ -109,9 +109,9 @@ public final class KeyUtils {
          * permissions are enforced on key files
          */
         public static final Set<PosixFilePermission> STRICTLY_PROHIBITED_FILE_PERMISSION =
    -            Collections.unmodifiableSet(
    -                    EnumSet.of(PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE,
    -                            PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE));
    +        Collections.unmodifiableSet(
    +            EnumSet.of(PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE,
    +                PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE));
     
         /**
          * System property that can be used to control the default fingerprint factory used for keys.
    
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractPrivateKeyObfuscator.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AESPrivateKeyObfuscator.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/DESPrivateKeyObfuscator.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHParserContext.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/DSSPEMResourceKeyPairParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/ECDSAPEMResourceKeyPairParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/KeyPairPEMResourceParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PEMResourceParserUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/RSAPEMResourceKeyPairParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyObfuscator.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryDecoder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryResolver.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryResolver.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/ListParseResult.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/LogLevelValue.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/NamedFactoriesListParseResult.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/NamedResourceListParseResult.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/SyslogFacilityValue.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/TimeValueConfig.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/config/VersionProperties.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/digest/BaseDigest.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/digest/BuiltinDigests.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/digest/DigestFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/digest/DigestInformation.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/digest/Digest.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/digest/DigestUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/digest/package.html+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/Factory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/future/AbstractSshFuture.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/future/CloseFuture.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/future/DefaultCloseFuture.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/future/DefaultSshFuture.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/future/DefaultVerifiableSshFuture.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/future/SshFuture.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/future/SshFutureListener.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/future/VerifiableFuture.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/future/WaitableFuture.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/keyprovider/AbstractKeyPairProvider.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/keyprovider/AbstractResourceKeyPairProvider.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/keyprovider/ClassLoadableResourceKeyPairProvider.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/keyprovider/FileKeyPairProvider.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyIdentityProvider.java+0 34 renamed
    @@ -26,7 +26,6 @@
     import java.util.function.Function;
     import java.util.function.Supplier;
     
    -import org.apache.sshd.client.session.ClientSession;
     import org.apache.sshd.common.util.GenericUtils;
     
     /**
    @@ -64,39 +63,6 @@ public String toString() {
          */
         Iterable<KeyPair> loadKeys();
     
    -    /**
    -     * Creates a &quot;unified&quot; {@link KeyIdentityProvider} of key pairs out of the registered
    -     * {@link KeyPair} identities and the extra available ones as a single iterator
    -     * of key pairs
    -     *
    -     *
    -     * @param session The {@link ClientSession} - ignored if {@code null} (i.e., empty
    -     * iterator returned)
    -     * @return The wrapping KeyIdentityProvider
    -     * @see ClientSession#getRegisteredIdentities()
    -     * @see ClientSession#getKeyPairProvider()
    -     */
    -    static KeyIdentityProvider providerOf(ClientSession session) {
    -        return session == null
    -                ? EMPTY_KEYS_PROVIDER
    -                : resolveKeyIdentityProvider(session.getRegisteredIdentities(), session.getKeyPairProvider());
    -    }
    -
    -    /**
    -     * Creates a &quot;unified&quot; {@link Iterator} of key pairs out of the registered
    -     * {@link KeyPair} identities and the extra available ones as a single iterator
    -     * of key pairs
    -     *
    -     * @param session The {@link ClientSession} - ignored if {@code null} (i.e., empty
    -     * iterator returned)
    -     * @return The wrapping iterator
    -     * @see ClientSession#getRegisteredIdentities()
    -     * @see ClientSession#getKeyPairProvider()
    -     */
    -    static Iterator<KeyPair> iteratorOf(ClientSession session) {
    -        return iteratorOf(providerOf(session));
    -    }
    -
         /**
          * Creates a &quot;unified&quot; {@link Iterator} of {@link KeyPair}s out of 2 possible
          * {@link KeyIdentityProvider}
    
  • sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProviderHolder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/keyprovider/KeyPairProvider.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/keyprovider/MappedKeyPairProvider.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/mac/BaseMac.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/mac/BuiltinMacs.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/mac/MacFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/mac/MacInformation.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/mac/Mac.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/mac/package.html+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/NamedFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/NamedResource.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/OptionalFeature.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/PropertyResolver.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/PropertyResolverUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/random/AbstractRandomFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/random/AbstractRandom.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/random/JceRandomFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/random/JceRandom.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/random/package.html+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/random/RandomFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/random/Random.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/random/SingletonRandomFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/RuntimeSshException.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/signature/AbstractSignature.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/signature/BuiltinSignatures.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/signature/package.html+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureDSA.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureECDSA.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureFactoriesManager.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/signature/Signature.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/signature/SignatureRSA.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/SshException.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/SyspropsMapWrapper.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferException.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/AbstractBufferPublicKeyParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/BufferPublicKeyParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/DSSBufferPublicKeyParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/ECBufferPublicKeyParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/ED25519BufferPublicKeyParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/RSABufferPublicKeyParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/closeable/AbstractCloseable.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/closeable/AbstractInnerCloseable.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/closeable/Builder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/closeable/FuturesCloseable.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/closeable/IoBaseCloseable.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/closeable/ParallelCloseable.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/closeable/SequentialCloseable.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/closeable/SimpleCloseable.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/EventListenerUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/EventNotifier.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/GenericUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/IgnoringEmptyMap.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/Int2IntFunction.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/Invoker.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/CloseableEmptyInputStream.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/der/ASN1Class.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/der/ASN1Object.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/der/ASN1Type.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/der/DERParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/der/DERWriter.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/DirectoryScanner.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/EmptyInputStream.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/FileInfoExtractor.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/InputStreamWithChannel.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/IoUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/LimitInputStream.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/LoggingFilterOutputStream.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/NoCloseInputStream.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/NoCloseOutputStream.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/NoCloseReader.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/NoCloseWriter.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/NullInputStream.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/NullOutputStream.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/io/OutputStreamWithChannel.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/logging/AbstractLoggingBean.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/logging/LoggingUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/logging/SimplifiedLog.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/MapEntryUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/net/NetworkConnector.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/NumberUtils.java+3 6 renamed
    @@ -34,18 +34,15 @@ public final class NumberUtils {
          * primitive numerical values
          */
         public static final List<Class<?>> NUMERIC_PRIMITIVE_CLASSES =
    -            GenericUtils.unmodifiableList(
    -                        Byte.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE,
    -                        Float.TYPE, Double.TYPE
    -                    );
    +        GenericUtils.unmodifiableList(
    +            Byte.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE, Float.TYPE, Double.TYPE);
     
         /**
          * A {@link List} containing all the pure powers of 2 for a {@code long}
          * value. The value at index <I>n</I> is 2 to the power of <I>n</I>
          */
         public static final List<Long> POWERS_OF_TWO =
    -            GenericUtils.unmodifiableList(IntStream.range(0, 64)
    -                    .mapToObj(i -> 1L << i));
    +        GenericUtils.unmodifiableList(IntStream.range(0, 64).mapToObj(i -> 1L << i));
     
         private NumberUtils() {
             throw new UnsupportedOperationException("No instance");
    
  • sshd-common/src/main/java/org/apache/sshd/common/util/ObjectBuilder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/OsUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/Readable.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/ReflectionUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/AbstractSecurityProviderRegistrar.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleGeneratorHostKeyProvider.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleKeyPairResourceParser.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleRandomFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleRandom.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleSecurityProviderRegistrar.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PublicKeyDecoder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderRegistrar.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/EdDSASecurityProviderUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/OpenSSHEd25519PrivateKeyEntryDecoder.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/SignatureEd25519.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityEntityFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityProviderChoice.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityProviderRegistrar.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/security/SecurityUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/SelectorUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/SshdEventListener.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/threads/CloseableExecutorService.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/threads/ExecutorServiceCarrier.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/threads/NoCloseExecutor.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/threads/SshdThreadFactory.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/threads/SshThreadPoolExecutor.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/threads/ThreadUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/ValidateUtils.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/common/util/VersionInfo.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/server/keyprovider/AbstractGeneratorHostKeyProvider.java+0 0 renamed
  • sshd-common/src/main/java/org/apache/sshd/server/keyprovider/SimpleGeneratorHostKeyProvider.java+0 0 renamed
  • sshd-common/src/test/java/org/apache/sshd/client/auth/password/PasswordIdentityProviderTest.java+3 4 renamed
    @@ -17,7 +17,7 @@
      * under the License.
      */
     
    -package org.apache.sshd.client.auth;
    +package org.apache.sshd.client.auth.password;
     
     import java.util.ArrayList;
     import java.util.Arrays;
    @@ -26,8 +26,7 @@
     import java.util.LinkedList;
     import java.util.List;
     
    -import org.apache.sshd.client.auth.password.PasswordIdentityProvider;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -39,7 +38,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class PasswordIdentityProviderTest extends BaseTestSupport {
    +public class PasswordIdentityProviderTest extends JUnitTestSupport {
         public PasswordIdentityProviderTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java+2 2 renamed
    @@ -29,7 +29,7 @@
     import java.util.concurrent.atomic.AtomicInteger;
     
     import org.apache.sshd.common.util.io.IoUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -41,7 +41,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class ConfigFileHostEntryResolverTest extends BaseTestSupport {
    +public class ConfigFileHostEntryResolverTest extends JUnitTestSupport {
         public ConfigFileHostEntryResolverTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java+2 2 renamed
    @@ -28,7 +28,7 @@
     import java.util.regex.Pattern;
     
     import org.apache.sshd.common.util.GenericUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -40,7 +40,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class HostConfigEntryTest extends BaseTestSupport {
    +public class HostConfigEntryTest extends JUnitTestSupport {
         public HostConfigEntryTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/client/config/hosts/KnownHostHashValueTest.java+2 2 renamed
    @@ -22,8 +22,8 @@
     import java.util.Arrays;
     import java.util.Collection;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
     import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -41,7 +41,7 @@
     @RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
     @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
     @Category({ NoIoTestCase.class })
    -public class KnownHostHashValueTest extends BaseTestSupport {
    +public class KnownHostHashValueTest extends JUnitTestSupport {
         private final String hostName;
         private final String hashValue;
         private final KnownHostHashValue hash;
    
  • sshd-common/src/test/java/org/apache/sshd/client/config/keys/BuiltinClientIdentitiesWatcherTest.java+4 4 renamed
    @@ -43,9 +43,9 @@
     import org.apache.sshd.common.util.GenericUtils;
     import org.apache.sshd.common.util.ValidateUtils;
     import org.apache.sshd.common.util.io.IoUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.CommonTestSupportUtils;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
    -import org.apache.sshd.util.test.Utils;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
     import org.junit.experimental.categories.Category;
    @@ -56,14 +56,14 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class BuiltinClientIdentitiesWatcherTest extends BaseTestSupport {
    +public class BuiltinClientIdentitiesWatcherTest extends JUnitTestSupport {
         public BuiltinClientIdentitiesWatcherTest() {
             super();
         }
     
         @Test
         public void testMultipleFilesWatch() throws Exception {
    -        KeyPair identity = Utils.getFirstKeyPair(createTestHostKeyProvider());
    +        KeyPair identity = CommonTestSupportUtils.getFirstKeyPair(createTestHostKeyProvider());
             String keyType = ValidateUtils.checkNotNullAndNotEmpty(KeyUtils.getKeyType(identity), "Cannot determine identity key type");
     
             Path dir = assertHierarchyTargetFolderExists(getTempTargetRelativeFile(getClass().getSimpleName()));
    
  • sshd-common/src/test/java/org/apache/sshd/client/config/keys/ClientIdentityFileWatcherTest.java+4 4 renamed
    @@ -34,9 +34,9 @@
     
     import org.apache.sshd.common.config.keys.FilePasswordProvider;
     import org.apache.sshd.common.util.io.IoUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.CommonTestSupportUtils;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
    -import org.apache.sshd.util.test.Utils;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
     import org.junit.experimental.categories.Category;
    @@ -47,7 +47,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class ClientIdentityFileWatcherTest extends BaseTestSupport {
    +public class ClientIdentityFileWatcherTest extends JUnitTestSupport {
         public ClientIdentityFileWatcherTest() {
             super();
         }
    @@ -56,7 +56,7 @@ public ClientIdentityFileWatcherTest() {
         public void testIdentityReload() throws Exception {
             Path dir = assertHierarchyTargetFolderExists(getTempTargetRelativeFile(getClass().getSimpleName()));
             Path idFile = dir.resolve(getCurrentTestName() + ".pem");
    -        KeyPair identity = Utils.getFirstKeyPair(createTestHostKeyProvider());
    +        KeyPair identity = CommonTestSupportUtils.getFirstKeyPair(createTestHostKeyProvider());
             ClientIdentityLoader loader = new ClientIdentityLoader() {
                 @Override
                 public KeyPair loadClientIdentity(String location, FilePasswordProvider provider) throws IOException, GeneralSecurityException {
    
  • sshd-common/src/test/java/org/apache/sshd/client/config/keys/ClientIdentityTest.java+2 2 renamed
    @@ -33,7 +33,7 @@
     import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
     import org.apache.sshd.common.util.GenericUtils;
     import org.apache.sshd.common.util.io.IoUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -45,7 +45,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class ClientIdentityTest extends BaseTestSupport {
    +public class ClientIdentityTest extends JUnitTestSupport {
         public ClientIdentityTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/cipher/AES192CTRTest.java+0 0 renamed
  • sshd-common/src/test/java/org/apache/sshd/common/cipher/AES256CBCTest.java+0 0 renamed
  • sshd-common/src/test/java/org/apache/sshd/common/cipher/ARCFOUR128Test.java+0 0 renamed
  • sshd-common/src/test/java/org/apache/sshd/common/cipher/ARCFOUR256Test.java+0 0 renamed
  • sshd-common/src/test/java/org/apache/sshd/common/cipher/BaseCipherTest.java+2 2 renamed
    @@ -29,7 +29,7 @@
     import org.apache.sshd.common.NamedFactory;
     import org.apache.sshd.common.cipher.Cipher.Mode;
     import org.apache.sshd.common.util.security.SecurityUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.Assume;
     import org.junit.experimental.categories.Category;
    @@ -38,7 +38,7 @@
      * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
      */
     @Category({ NoIoTestCase.class })
    -public abstract class BaseCipherTest extends BaseTestSupport {
    +public abstract class BaseCipherTest extends JUnitTestSupport {
         protected BaseCipherTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/cipher/ECCurvesTest.java+2 2 renamed
    @@ -22,7 +22,7 @@
     import java.util.EnumSet;
     import java.util.Set;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -34,7 +34,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class ECCurvesTest extends BaseTestSupport {
    +public class ECCurvesTest extends JUnitTestSupport {
         public ECCurvesTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/compression/BuiltinCompressionsTest.java+2 2 renamed
    @@ -32,7 +32,7 @@
     import org.apache.sshd.common.NamedResource;
     import org.apache.sshd.common.compression.BuiltinCompressions.ParseResult;
     import org.apache.sshd.common.util.GenericUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -45,7 +45,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class BuiltinCompressionsTest extends BaseTestSupport {
    +public class BuiltinCompressionsTest extends JUnitTestSupport {
         public BuiltinCompressionsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryLoginOptionsParseTest.java+2 2 renamed
    @@ -24,8 +24,8 @@
     import java.util.Map;
     
     import org.apache.sshd.common.util.GenericUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
     import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
     import org.junit.runner.RunWith;
    @@ -40,7 +40,7 @@
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
     @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
    -public class AuthorizedKeyEntryLoginOptionsParseTest extends BaseTestSupport {
    +public class AuthorizedKeyEntryLoginOptionsParseTest extends JUnitTestSupport {
         private final String value;
         private final String loginPart;
         private final String keyPart;
    
  • sshd-common/src/test/java/org/apache/sshd/common/config/keys/BuiltinIdentitiesTest.java+2 2 renamed
    @@ -27,8 +27,8 @@
     import java.util.List;
     
     import org.apache.sshd.common.util.security.SecurityUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
     import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.Assume;
     import org.junit.BeforeClass;
    @@ -48,7 +48,7 @@
     @RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
     @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
     @Category({ NoIoTestCase.class })
    -public class BuiltinIdentitiesTest extends BaseTestSupport {
    +public class BuiltinIdentitiesTest extends JUnitTestSupport {
         private final BuiltinIdentities expected;
     
         public BuiltinIdentitiesTest(BuiltinIdentities expected) {
    
  • sshd-common/src/test/java/org/apache/sshd/common/config/keys/KeyRandomArtTest.java+4 4 renamed
    @@ -28,10 +28,10 @@
     import org.apache.sshd.common.cipher.ECCurves;
     import org.apache.sshd.common.util.GenericUtils;
     import org.apache.sshd.common.util.security.SecurityUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.CommonTestSupportUtils;
     import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
    -import org.apache.sshd.util.test.Utils;
     import org.junit.AfterClass;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -49,7 +49,7 @@
     @RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
     @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
     @Category({ NoIoTestCase.class })
    -public class KeyRandomArtTest extends BaseTestSupport {
    +public class KeyRandomArtTest extends JUnitTestSupport {
         private static final Collection<KeyPair> KEYS = new LinkedList<>();
     
         private final String algorithm;
    @@ -59,7 +59,7 @@ public class KeyRandomArtTest extends BaseTestSupport {
         public KeyRandomArtTest(String algorithm, int keySize) throws Exception {
             this.algorithm = algorithm;
             this.keySize = keySize;
    -        this.keyPair = Utils.generateKeyPair(algorithm, keySize);
    +        this.keyPair = CommonTestSupportUtils.generateKeyPair(algorithm, keySize);
             KEYS.add(this.keyPair);
         }
     
    
  • sshd-common/src/test/java/org/apache/sshd/common/config/keys/KeyUtilsCloneTest.java+2 2 renamed
    @@ -32,8 +32,8 @@
     import org.apache.sshd.common.keyprovider.KeyPairProvider;
     import org.apache.sshd.common.util.ValidateUtils;
     import org.apache.sshd.common.util.security.SecurityUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
     import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -51,7 +51,7 @@
     @RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
     @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
     @Category({ NoIoTestCase.class })
    -public class KeyUtilsCloneTest extends BaseTestSupport {
    +public class KeyUtilsCloneTest extends JUnitTestSupport {
         private final String keyType;
         private final int keySize;
     
    
  • sshd-common/src/test/java/org/apache/sshd/common/config/keys/KeyUtilsFingerprintCaseSensitivityTest.java+2 2 renamed
    @@ -26,8 +26,8 @@
     import java.util.Arrays;
     import java.util.Collection;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
     import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.BeforeClass;
     import org.junit.FixMethodOrder;
    @@ -46,7 +46,7 @@
     @RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
     @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
     @Category({ NoIoTestCase.class })
    -public class KeyUtilsFingerprintCaseSensitivityTest extends BaseTestSupport {
    +public class KeyUtilsFingerprintCaseSensitivityTest extends JUnitTestSupport {
     
         // CHECKSTYLE:OFF
         private static final String KEY_STRING = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAxr3N5fkt966xJINl0hH7Q6lLDRR1D0yMjcXCE5roE9VFut2ctGFuo90TCOxkPOMnwzwConeyScVF4ConZeWsxbG9VtRh61IeZ6R5P5ZTvE9xPdZBgIEWvU1bRfrrOfSMihqF98pODspE6NoTtND2eglwSGwxcYFmpdTAmu+8qgxgGxlEaaCjqwdiNPZhygrH81Mv2ruolNeZkn4Bj+wFFmZTD/waN1pQaMf+SO1+kEYIYFNl5+8JRGuUcr8MhHHJB+gwqMTF2BSBVITJzZUiQR0TMtkK6Vbs7yt1F9hhzDzAFDwhV+rsfNQaOHpl3zP07qH+/99A0XG1CVcEdHqVMw== lgoldstein@LGOLDSTEIN-WIN7";
    
  • sshd-common/src/test/java/org/apache/sshd/common/config/keys/KeyUtilsFingerprintGenerationTest.java+2 2 renamed
    @@ -33,8 +33,8 @@
     
     import org.apache.sshd.common.digest.BuiltinDigests;
     import org.apache.sshd.common.digest.DigestFactory;
    -import org.apache.sshd.util.test.BaseTestSupport;
     import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -52,7 +52,7 @@
     @RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
     @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
     @Category({ NoIoTestCase.class })
    -public class KeyUtilsFingerprintGenerationTest extends BaseTestSupport {
    +public class KeyUtilsFingerprintGenerationTest extends JUnitTestSupport {
         private final PublicKey key;
         private final DigestFactory digestFactory;
         private final String expected;
    
  • sshd-common/src/test/java/org/apache/sshd/common/config/keys/KeyUtilsTest.java+2 2 renamed
    @@ -37,7 +37,7 @@
     import org.apache.sshd.common.util.GenericUtils;
     import org.apache.sshd.common.util.OsUtils;
     import org.apache.sshd.common.util.io.IoUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -49,7 +49,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class KeyUtilsTest extends BaseTestSupport {
    +public class KeyUtilsTest extends JUnitTestSupport {
         public KeyUtilsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/AESPrivateKeyObfuscatorTest.java+2 2 renamed
    @@ -28,8 +28,8 @@
     
     import org.apache.sshd.common.util.GenericUtils;
     import org.apache.sshd.common.util.security.SecurityUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
     import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -47,7 +47,7 @@
     @RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
     @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
     @Category({ NoIoTestCase.class })
    -public class AESPrivateKeyObfuscatorTest extends BaseTestSupport {
    +public class AESPrivateKeyObfuscatorTest extends JUnitTestSupport {
         private static final Random RANDOMIZER = new Random(System.currentTimeMillis());
     
         private final int keyLength;
    
  • sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParserTest.java+2 2 renamed
    @@ -32,8 +32,8 @@
     import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
     import org.apache.sshd.common.util.GenericUtils;
     import org.apache.sshd.common.util.ValidateUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
     import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.Assume;
     import org.junit.FixMethodOrder;
    @@ -52,7 +52,7 @@
     @RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
     @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
     @Category({ NoIoTestCase.class })
    -public class OpenSSHKeyPairResourceParserTest extends BaseTestSupport {
    +public class OpenSSHKeyPairResourceParserTest extends JUnitTestSupport {
         private static final OpenSSHKeyPairResourceParser PARSER = OpenSSHKeyPairResourceParser.INSTANCE;
         private final BuiltinIdentities identity;
     
    
  • sshd-common/src/test/java/org/apache/sshd/common/config/keys/loader/pem/PKCS8PEMResourceKeyPairParserTest.java+2 2 renamed
    @@ -33,8 +33,8 @@
     import org.apache.commons.ssl.PEMUtil;
     import org.apache.sshd.common.config.keys.KeyUtils;
     import org.apache.sshd.common.util.security.SecurityUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
     import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -54,7 +54,7 @@
     @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class PKCS8PEMResourceKeyPairParserTest extends BaseTestSupport {
    +public class PKCS8PEMResourceKeyPairParserTest extends JUnitTestSupport {
         private final String algorithm;
         private final int keySize;
     
    
  • sshd-common/src/test/java/org/apache/sshd/common/config/keys/PublicKeyEntryTest.java+2 2 renamed
    @@ -25,7 +25,7 @@
     import java.util.Arrays;
     
     import org.apache.sshd.common.util.GenericUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -37,7 +37,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class PublicKeyEntryTest extends BaseTestSupport {
    +public class PublicKeyEntryTest extends JUnitTestSupport {
         public PublicKeyEntryTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/config/TimeValueConfigTest.java+2 2 renamed
    @@ -21,7 +21,7 @@
     
     import java.util.concurrent.TimeUnit;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -33,7 +33,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class TimeValueConfigTest extends BaseTestSupport {
    +public class TimeValueConfigTest extends JUnitTestSupport {
         public TimeValueConfigTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/digest/BuiltinDigestsTest.java+2 2 renamed
    @@ -23,7 +23,7 @@
     import java.util.EnumSet;
     import java.util.Set;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -35,7 +35,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class BuiltinDigestsTest extends BaseTestSupport {
    +public class BuiltinDigestsTest extends JUnitTestSupport {
         public BuiltinDigestsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/future/DefaultSshFutureTest.java+2 2 renamed
    @@ -21,7 +21,7 @@
     import java.util.concurrent.TimeUnit;
     import java.util.concurrent.atomic.AtomicInteger;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -34,7 +34,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class DefaultSshFutureTest extends BaseTestSupport {
    +public class DefaultSshFutureTest extends JUnitTestSupport {
         public DefaultSshFutureTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/keyprovider/KeyPairProviderTest.java+2 2 renamed
    @@ -27,7 +27,7 @@
     import java.util.function.Function;
     
     import org.apache.sshd.common.util.GenericUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -40,7 +40,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class KeyPairProviderTest extends BaseTestSupport {
    +public class KeyPairProviderTest extends JUnitTestSupport {
         public KeyPairProviderTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/mac/BuiltinMacsTest.java+2 2 renamed
    @@ -31,7 +31,7 @@
     import org.apache.sshd.common.NamedResource;
     import org.apache.sshd.common.mac.BuiltinMacs.ParseResult;
     import org.apache.sshd.common.util.GenericUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -44,7 +44,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class BuiltinMacsTest extends BaseTestSupport {
    +public class BuiltinMacsTest extends JUnitTestSupport {
         public BuiltinMacsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/mac/MacVectorsTest.java+2 2 renamed
    @@ -33,8 +33,8 @@
     import org.apache.sshd.common.util.GenericUtils;
     import org.apache.sshd.common.util.ValidateUtils;
     import org.apache.sshd.common.util.buffer.BufferUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
     import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -53,7 +53,7 @@
     @RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
     @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
     @Category({ NoIoTestCase.class })
    -public class MacVectorsTest extends BaseTestSupport {
    +public class MacVectorsTest extends JUnitTestSupport {
         private final VectorSeed seed;
         private final Factory<? extends Mac> macFactory;
         private final byte[] expected;
    
  • sshd-common/src/test/java/org/apache/sshd/common/random/RandomFactoryTest.java+2 2 renamed
    @@ -23,8 +23,8 @@
     import java.util.concurrent.TimeUnit;
     
     import org.apache.sshd.common.util.security.SecurityUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
     import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.Assume;
     import org.junit.FixMethodOrder;
    @@ -43,7 +43,7 @@
     @RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
     @UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
     @Category({ NoIoTestCase.class })
    -public class RandomFactoryTest extends BaseTestSupport {
    +public class RandomFactoryTest extends JUnitTestSupport {
         private final RandomFactory factory;
     
         public RandomFactoryTest(RandomFactory factory) {
    
  • sshd-common/src/test/java/org/apache/sshd/common/signature/BuiltinSignaturesTest.java+2 2 renamed
    @@ -28,7 +28,7 @@
     import org.apache.sshd.common.NamedResource;
     import org.apache.sshd.common.signature.BuiltinSignatures.ParseResult;
     import org.apache.sshd.common.util.GenericUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -41,7 +41,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class BuiltinSignaturesTest extends BaseTestSupport {
    +public class BuiltinSignaturesTest extends JUnitTestSupport {
         public BuiltinSignaturesTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/signature/SignatureDSATest.java+2 2 renamed
    @@ -25,7 +25,7 @@
     
     import org.apache.sshd.common.config.keys.KeyUtils;
     import org.apache.sshd.common.util.security.SecurityUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -37,7 +37,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class SignatureDSATest extends BaseTestSupport {
    +public class SignatureDSATest extends JUnitTestSupport {
         public SignatureDSATest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/signature/SignatureRSATest.java+2 2 renamed
    @@ -30,7 +30,7 @@
     import org.apache.sshd.common.Factory;
     import org.apache.sshd.common.config.keys.KeyUtils;
     import org.apache.sshd.common.util.security.SecurityUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.junit.BeforeClass;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -40,7 +40,7 @@
      * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
    -public class SignatureRSATest extends BaseTestSupport {
    +public class SignatureRSATest extends JUnitTestSupport {
         private static final Base64.Decoder B64_DECODER = Base64.getDecoder();
         @SuppressWarnings("checkstyle:linelength")
         private static final byte[] TEST_MSG =
    
  • sshd-common/src/test/java/org/apache/sshd/common/signature/SignaturesDevelopment.java+4 2 renamed
    @@ -26,13 +26,15 @@
     import org.apache.sshd.common.util.GenericUtils;
     import org.apache.sshd.common.util.buffer.BufferUtils;
     import org.apache.sshd.common.util.security.eddsa.EdDSASecurityProviderUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
    +import org.junit.Ignore;
     
     /**
      * A &quot;scratch-pad&quot; class for testing signatures related code during development
      * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
      */
    -public class SignaturesDevelopment extends BaseTestSupport {
    +@Ignore("Used only for development")
    +public class SignaturesDevelopment extends JUnitTestSupport {
         public SignaturesDevelopment() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/SshConstantsTest.java+2 2 renamed
    @@ -22,7 +22,7 @@
     import java.util.Collection;
     
     import org.apache.sshd.common.util.GenericUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -34,7 +34,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class SshConstantsTest extends BaseTestSupport {
    +public class SshConstantsTest extends JUnitTestSupport {
         public SshConstantsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/buffer/BufferTest.java+2 2 renamed
    @@ -23,7 +23,7 @@
     import java.nio.charset.StandardCharsets;
     
     import org.apache.sshd.common.util.GenericUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -32,7 +32,7 @@
     
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class BufferTest extends BaseTestSupport {
    +public class BufferTest extends JUnitTestSupport {
         public BufferTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/buffer/BufferUtilsTest.java+2 2 renamed
    @@ -22,7 +22,7 @@
     import java.nio.charset.StandardCharsets;
     import java.util.Random;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -34,7 +34,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class BufferUtilsTest extends BaseTestSupport {
    +public class BufferUtilsTest extends JUnitTestSupport {
         public BufferUtilsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/closeable/CloseableUtilsTest.java+2 2 renamed
    @@ -30,7 +30,7 @@
     import org.apache.sshd.common.future.DefaultCloseFuture;
     import org.apache.sshd.common.future.SshFutureListener;
     import org.apache.sshd.common.util.threads.ThreadUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -42,7 +42,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class CloseableUtilsTest extends BaseTestSupport {
    +public class CloseableUtilsTest extends JUnitTestSupport {
         public CloseableUtilsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/EventListenerUtilsTest.java+2 2 renamed
    @@ -26,7 +26,7 @@
     import java.util.List;
     import java.util.Set;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -38,7 +38,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class EventListenerUtilsTest extends BaseTestSupport {
    +public class EventListenerUtilsTest extends JUnitTestSupport {
         public EventListenerUtilsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/GenericUtilsTest.java+3 3 renamed
    @@ -24,7 +24,7 @@
     import java.util.List;
     import java.util.NoSuchElementException;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -35,8 +35,8 @@
      * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
    -@Category({ NoIoTestCase.class })
    -public class GenericUtilsTest extends BaseTestSupport {
    +@Category(NoIoTestCase.class)
    +public class GenericUtilsTest extends JUnitTestSupport {
         public GenericUtilsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/Int2IntFunctionTest.java+2 2 renamed
    @@ -22,7 +22,7 @@
     import java.util.Random;
     import java.util.function.IntUnaryOperator;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -34,7 +34,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class Int2IntFunctionTest extends BaseTestSupport {
    +public class Int2IntFunctionTest extends JUnitTestSupport {
         public Int2IntFunctionTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/io/EmptyInputStreamTest.java+2 2 renamed
    @@ -23,7 +23,7 @@
     import java.io.InputStream;
     
     import org.apache.sshd.common.util.buffer.BufferUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -35,7 +35,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class EmptyInputStreamTest extends BaseTestSupport {
    +public class EmptyInputStreamTest extends JUnitTestSupport {
         public EmptyInputStreamTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/io/IoUtilsTest.java+2 2 renamed
    @@ -22,7 +22,7 @@
     import java.nio.file.LinkOption;
     
     import org.apache.sshd.common.util.NumberUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -34,7 +34,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class IoUtilsTest extends BaseTestSupport {
    +public class IoUtilsTest extends JUnitTestSupport {
         public IoUtilsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/io/LimitInputStreamTest.java+2 2 renamed
    @@ -25,7 +25,7 @@
     import java.nio.file.Files;
     import java.nio.file.Path;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -37,7 +37,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class LimitInputStreamTest extends BaseTestSupport {
    +public class LimitInputStreamTest extends JUnitTestSupport {
         public LimitInputStreamTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/io/ModifiableFileWatcherTest.java+2 2 renamed
    @@ -31,7 +31,7 @@
     
     import org.apache.sshd.common.util.GenericUtils;
     import org.apache.sshd.common.util.OsUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -43,7 +43,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class ModifiableFileWatcherTest extends BaseTestSupport {
    +public class ModifiableFileWatcherTest extends JUnitTestSupport {
         public ModifiableFileWatcherTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/NumberUtilsTest.java+2 2 renamed
    @@ -19,7 +19,7 @@
     
     package org.apache.sshd.common.util;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -31,7 +31,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class NumberUtilsTest extends BaseTestSupport {
    +public class NumberUtilsTest extends JUnitTestSupport {
         public NumberUtilsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/OsUtilsTest.java+2 2 renamed
    @@ -21,7 +21,7 @@
     
     import java.util.Objects;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -33,7 +33,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class OsUtilsTest extends BaseTestSupport {
    +public class OsUtilsTest extends JUnitTestSupport {
         public OsUtilsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/SelectorUtilsTest.java+2 2 renamed
    @@ -21,7 +21,7 @@
     import java.io.File;
     import java.util.Random;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.Assume;
     import org.junit.FixMethodOrder;
    @@ -34,7 +34,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class SelectorUtilsTest extends BaseTestSupport {
    +public class SelectorUtilsTest extends JUnitTestSupport {
         public SelectorUtilsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/ThreadUtilsTest.java+2 2 renamed
    @@ -23,7 +23,7 @@
     
     import org.apache.sshd.common.util.threads.CloseableExecutorService;
     import org.apache.sshd.common.util.threads.ThreadUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -35,7 +35,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class ThreadUtilsTest extends BaseTestSupport {
    +public class ThreadUtilsTest extends JUnitTestSupport {
         public ThreadUtilsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/ValidateUtilsTest.java+3 3 renamed
    @@ -19,7 +19,7 @@
     
     package org.apache.sshd.common.util;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -30,8 +30,8 @@
      * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
    -@Category({ NoIoTestCase.class })
    -public class ValidateUtilsTest extends BaseTestSupport {
    +@Category(NoIoTestCase.class)
    +public class ValidateUtilsTest extends JUnitTestSupport {
         public ValidateUtilsTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/util/VersionInfoTest.java+2 2 renamed
    @@ -19,7 +19,7 @@
     
     package org.apache.sshd.common.util;
     
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.apache.sshd.util.test.NoIoTestCase;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
    @@ -31,7 +31,7 @@
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
     @Category({ NoIoTestCase.class })
    -public class VersionInfoTest extends BaseTestSupport {
    +public class VersionInfoTest extends JUnitTestSupport {
         public VersionInfoTest() {
             super();
         }
    
  • sshd-common/src/test/java/org/apache/sshd/common/VersionPropertiesTest.java+2 2 renamed
    @@ -23,7 +23,7 @@
     
     import org.apache.sshd.common.config.VersionProperties;
     import org.apache.sshd.common.util.GenericUtils;
    -import org.apache.sshd.util.test.BaseTestSupport;
    +import org.apache.sshd.util.test.JUnitTestSupport;
     import org.junit.FixMethodOrder;
     import org.junit.Test;
     import org.junit.runners.MethodSorters;
    @@ -32,7 +32,7 @@
      * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
      */
     @FixMethodOrder(MethodSorters.NAME_ASCENDING)
    -public class VersionPropertiesTest extends BaseTestSupport {
    +public class VersionPropertiesTest extends JUnitTestSupport {
         public VersionPropertiesTest() {
             super();
         }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.