org.apache.sshd.common.file.root.RootedFileSystemProvider Maven / Gradle / Ivy
Go to download
This artifact provides a single jar that contains all classes required to use remote EJB and JMS, including
all dependencies. It is intended for use by those not using maven, maven users should just import the EJB and
JMS BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up
with different versions on classes on the class path).
The newest version!
/*
* 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.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
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.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import org.apache.sshd.common.util.io.IoUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* File system provider which provides a rooted file system. The file system only gives access to files under the root
* directory.
*
* @author Apache MINA SSHD Project
*/
public class RootedFileSystemProvider extends FileSystemProvider {
protected final Logger log;
private final Map fileSystems = new HashMap<>();
public RootedFileSystemProvider() {
log = LoggerFactory.getLogger(getClass());
}
@Override
public String getScheme() {
return "root";
}
@Override
public FileSystem newFileSystem(URI uri, Map env) throws IOException {
return newFileSystem(uri, uriToPath(uri), env);
}
@Override
public FileSystem getFileSystem(URI uri) {
return getFileSystem(uriToPath(uri));
}
@Override
public FileSystem newFileSystem(Path path, Map env) throws IOException {
return newFileSystem(path, path, env);
}
protected FileSystem newFileSystem(Object src, Path path, Map env) throws IOException {
Path root = ensureDirectory(path).toRealPath();
RootedFileSystem rootedFs = null;
synchronized (fileSystems) {
if (!this.fileSystems.containsKey(root)) {
rootedFs = new RootedFileSystem(this, path, env);
this.fileSystems.put(root, rootedFs);
}
}
// do all the throwing outside the synchronized block to minimize its lock time
if (rootedFs == null) {
throw new FileSystemAlreadyExistsException("newFileSystem(" + src + ") already mapped " + root);
}
if (log.isTraceEnabled()) {
log.trace("newFileSystem({}): {}", src, rootedFs);
}
return rootedFs;
}
protected Path uriToPath(URI uri) {
String scheme = uri.getScheme();
String expected = getScheme();
if ((scheme == null) || (!scheme.equalsIgnoreCase(expected))) {
throw new IllegalArgumentException("URI scheme (" + scheme + ") is not '" + expected + "'");
}
String root = uri.getRawSchemeSpecificPart();
int i = root.indexOf("!/");
if (i != -1) {
root = root.substring(0, i);
}
try {
return Paths.get(new URI(root)).toAbsolutePath();
} catch (URISyntaxException e) {
throw new IllegalArgumentException(root + ": " + e.getMessage(), e);
}
}
private static Path ensureDirectory(Path path) {
return IoUtils.ensureDirectory(path, IoUtils.getLinkOptions(true));
}
@Override
public Path getPath(URI uri) {
String str = uri.getSchemeSpecificPart();
int i = str.indexOf("!/");
if (i == -1) {
throw new IllegalArgumentException("URI: " + uri + " does not contain path info - e.g., root:file://foo/bar!/");
}
FileSystem fs = getFileSystem(uri);
String subPath = str.substring(i + 1);
Path p = fs.getPath(subPath);
if (log.isTraceEnabled()) {
log.trace("getPath({}): {}", uri, p);
}
return p;
}
@Override
public InputStream newInputStream(Path path, OpenOption... options) throws IOException {
Path r = unroot(path);
FileSystemProvider p = provider(r);
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);
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);
try {
return p.newFileChannel(r, options, attrs);
} catch (IOException ex) {
throw translateIoException(ex, path);
}
}
@Override
public AsynchronousFileChannel newAsynchronousFileChannel(
Path path, Set extends OpenOption> options, ExecutorService executor, FileAttribute>... attrs)
throws IOException {
Path r = unroot(path);
FileSystemProvider p = provider(r);
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);
try {
return p.newByteChannel(r, options, attrs);
} catch (IOException ex) {
throw translateIoException(ex, path);
}
}
@Override
public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter super Path> filter) throws IOException {
Path r = unroot(dir);
FileSystemProvider p = provider(r);
try {
return root(((RootedPath) dir).getFileSystem(), p.newDirectoryStream(r, filter));
} catch (IOException ex) {
throw translateIoException(ex, dir);
}
}
protected DirectoryStream root(RootedFileSystem rfs, DirectoryStream ds) {
if (ds instanceof SecureDirectoryStream) {
return new RootedSecureDirectoryStream(rfs, (SecureDirectoryStream) ds);
}
return new RootedDirectoryStream(rfs, ds);
}
@Override
public void createDirectory(Path dir, FileAttribute>... attrs) throws IOException {
Path r = unroot(dir);
FileSystemProvider p = provider(r);
try {
p.createDirectory(r, attrs);
} catch (IOException ex) {
throw translateIoException(ex, dir);
}
}
@Override
public void createSymbolicLink(Path link, Path target, 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 = target.isAbsolute() ? unroot(target) : l.getFileSystem().getPath(target.toString());
FileSystemProvider p = provider(l);
try {
p.createSymbolicLink(l, t, attrs);
if (log.isDebugEnabled()) {
log.debug("createSymbolicLink({} => {}", l, t);
}
} catch (IOException ex) {
throw translateIoException(ex, link);
}
}
@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);
}
}
@Override
public void delete(Path path) throws IOException {
Path r = unroot(path);
if (log.isTraceEnabled()) {
log.trace("delete({}): {}", path, r);
}
FileSystemProvider p = provider(r);
try {
p.delete(r);
} catch (IOException ex) {
throw translateIoException(ex, path);
}
}
@Override
public boolean deleteIfExists(Path path) throws IOException {
Path r = unroot(path);
if (log.isTraceEnabled()) {
log.trace("deleteIfExists({}): {}", path, r);
}
FileSystemProvider p = provider(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);
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);
}
}
@Override
public void copy(Path source, Path target, CopyOption... options) throws IOException {
Path s = unroot(source);
Path t = unroot(target);
if (log.isTraceEnabled()) {
log.trace("copy({})[{}]: {}[{}]", source, s, target, t);
}
FileSystemProvider p = provider(s);
try {
p.copy(s, t, options);
} catch (IOException ex) {
throw translateIoException(ex, source);
}
}
@Override
public void move(Path source, Path target, CopyOption... options) throws IOException {
Path s = unroot(source);
Path t = unroot(target);
if (log.isTraceEnabled()) {
log.trace("move({})[{}]: {}[{}]", source, s, target, t);
}
FileSystemProvider p = provider(s);
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);
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);
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();
try {
return Files.getFileStore(root);
} catch (IOException ex) {
throw translateIoException(ex, path);
}
}
protected RootedFileSystem getFileSystem(Path path) throws FileSystemNotFoundException {
Path real = unroot(path);
Path rootInstance = null;
RootedFileSystem fsInstance = null;
synchronized (fileSystems) {
// Cannot use forEach because the referenced variable are not effectively final
for (Map.Entry fse : fileSystems.entrySet()) {
Path root = fse.getKey();
RootedFileSystem fs = fse.getValue();
if (real.equals(root)) {
return fs; // we were lucky to have the root
}
if (!real.startsWith(root)) {
continue;
}
// if already have a candidate prefer the longer match since both are prefixes of the real path
if ((rootInstance == null) || (rootInstance.getNameCount() < root.getNameCount())) {
rootInstance = root;
fsInstance = fs;
}
}
}
if (fsInstance == null) {
throw new FileSystemNotFoundException(path.toString());
}
if (log.isTraceEnabled()) {
log.trace("getFileSystem({}): {}", path, fsInstance);
}
return fsInstance;
}
@Override
public void checkAccess(Path path, AccessMode... modes) throws IOException {
Path r = unroot(path);
FileSystemProvider p = provider(r);
try {
p.checkAccess(r, modes);
} catch (IOException ex) {
throw translateIoException(ex, path);
}
}
@Override
public V getFileAttributeView(Path path, Class type, LinkOption... options) {
Path r = unroot(path);
FileSystemProvider p = provider(r);
return p.getFileAttributeView(r, type, options);
}
@Override
public A readAttributes(Path path, Class type, LinkOption... options)
throws IOException {
Path r = unroot(path);
if (log.isTraceEnabled()) {
log.trace("readAttributes({})[{}] type={}", path, r, type.getSimpleName());
}
FileSystemProvider p = provider(r);
try {
return p.readAttributes(r, type, options);
} catch (IOException ex) {
throw translateIoException(ex, path);
}
}
@Override
public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
Path r = unroot(path);
FileSystemProvider p = provider(r);
try {
Map 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);
}
}
@Override
public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
Path r = unroot(path);
if (log.isTraceEnabled()) {
log.trace("setAttribute({})[{}] {}={}", path, r, attribute, value);
}
FileSystemProvider p = provider(r);
try {
p.setAttribute(r, attribute, value, options);
} catch (IOException ex) {
throw translateIoException(ex, path);
}
}
protected FileSystemProvider provider(Path path) {
FileSystem fs = path.getFileSystem();
return fs.provider();
}
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);
} 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());
}
}
/**
* @param path The original (rooted) {@link Path}
* @return The actual absolute local {@link Path} represented by the rooted
* one
* @see #resolveLocalPath(RootedPath)
* @throws IllegalArgumentException if {@code null} path argument
* @throws ProviderMismatchException if not a {@link RootedPath}
*/
protected Path unroot(Path path) {
Objects.requireNonNull(path, "No path to unroot");
if (!(path instanceof RootedPath)) {
throw new ProviderMismatchException("unroot(" + path + ") is not a " + RootedPath.class.getSimpleName()
+ " but rather a " + path.getClass().getSimpleName());
}
return resolveLocalPath((RootedPath) path);
}
/**
* @param path The original {@link RootedPath} - never {@code null}
* @return The actual absolute local {@link Path} represented by the rooted one
* @throws InvalidPathException If the resolved path is not a proper sub-path of the rooted file system
*/
protected Path resolveLocalPath(RootedPath path) {
Objects.requireNonNull(path, "No rooted path to resolve");
RootedFileSystem rfs = path.getFileSystem();
Path root = rfs.getRoot();
// initialize a list for the new file name parts
Path resolved = IoUtils.chroot(root, path);
/*
* 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;
}
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;
}
private String fixExceptionFileName(Path root, Path rootedPath, String fileName) {
if (fileName == null) {
return null;
}
Path toFix = root.getFileSystem().getPath(fileName);
if (toFix.getNameCount() == root.getNameCount()) {
// return the root
return rootedPath.getFileSystem().getSeparator();
}
StringBuilder ret = new StringBuilder();
for (int partNum = root.getNameCount(); partNum < toFix.getNameCount(); partNum++) {
ret.append(rootedPath.getFileSystem().getSeparator());
ret.append(toFix.getName(partNum++));
}
return ret.toString();
}
}