io.undertow.server.handlers.resource.PathResourceManager 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).
package io.undertow.server.handlers.resource;
import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;
import io.undertow.util.ETag;
import org.jboss.logging.Logger;
import org.xnio.FileChangeCallback;
import org.xnio.FileChangeEvent;
import org.xnio.FileSystemWatcher;
import org.xnio.OptionMap;
import org.xnio.Xnio;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.TreeSet;
/**
* Serves files from the file system.
*/
public class PathResourceManager implements ResourceManager {
private static final Logger log = Logger.getLogger(PathResourceManager.class.getName());
private static final boolean DEFAULT_CHANGE_LISTENERS_ALLOWED = !Boolean.getBoolean("io.undertow.disable-file-system-watcher");
private static final long DEFAULT_TRANSFER_MIN_SIZE = 1024;
private static final ETagFunction NULL_ETAG_FUNCTION = new ETagFunction() {
@Override
public ETag generate(Path path) {
return null;
}
};
private final List listeners = new ArrayList<>();
private FileSystemWatcher fileSystemWatcher;
protected volatile String base;
protected volatile FileSystem fileSystem;
/**
* Size to use direct FS to network transfer (if supported by OS/JDK) instead of read/write
*/
private final long transferMinSize;
/**
* Check to validate caseSensitive issues for specific case-insensitive FS.
* @see io.undertow.server.handlers.resource.PathResourceManager#isFileSameCase(java.nio.file.Path, String)
*/
private final boolean caseSensitive;
/**
* Check to allow follow symbolic links
*/
private final boolean followLinks;
/**
* Used if followLinks == true. Set of paths valid to follow symbolic links. If this is empty and followLinks
* it true then all links will be followed
*/
private final TreeSet safePaths = new TreeSet<>();
private final ETagFunction eTagFunction;
private final boolean allowResourceChangeListeners;
public PathResourceManager(final Path base) {
this(base, DEFAULT_TRANSFER_MIN_SIZE, true, false, null);
}
public PathResourceManager(final Path base, long transferMinSize) {
this(base, transferMinSize, true, false, null);
}
public PathResourceManager(final Path base, long transferMinSize, boolean caseSensitive) {
this(base, transferMinSize, caseSensitive, false, null);
}
public PathResourceManager(final Path base, long transferMinSize, boolean followLinks, final String... safePaths) {
this(base, transferMinSize, true, followLinks, safePaths);
}
protected PathResourceManager(long transferMinSize, boolean caseSensitive, boolean followLinks, final String... safePaths) {
this(transferMinSize, caseSensitive, followLinks, DEFAULT_CHANGE_LISTENERS_ALLOWED, safePaths);
}
protected PathResourceManager(long transferMinSize, boolean caseSensitive, boolean followLinks, boolean allowResourceChangeListeners, final String... safePaths) {
this.fileSystem = FileSystems.getDefault();
this.caseSensitive = caseSensitive;
this.followLinks = followLinks;
this.transferMinSize = transferMinSize;
this.allowResourceChangeListeners = allowResourceChangeListeners;
if (this.followLinks) {
if (safePaths == null) {
throw UndertowMessages.MESSAGES.argumentCannotBeNull("safePaths");
}
for (final String safePath : safePaths) {
if (safePath == null) {
throw UndertowMessages.MESSAGES.argumentCannotBeNull("safePaths");
}
}
this.safePaths.addAll(Arrays.asList(safePaths));
}
this.eTagFunction = NULL_ETAG_FUNCTION;
}
public PathResourceManager(final Path base, long transferMinSize, boolean caseSensitive, boolean followLinks, final String... safePaths) {
this(base, transferMinSize, caseSensitive, followLinks, DEFAULT_CHANGE_LISTENERS_ALLOWED, safePaths);
}
public PathResourceManager(final Path base, long transferMinSize, boolean caseSensitive, boolean followLinks, boolean allowResourceChangeListeners, final String... safePaths) {
this(builder()
.setBase(base)
.setTransferMinSize(transferMinSize)
.setCaseSensitive(caseSensitive)
.setFollowLinks(followLinks)
.setAllowResourceChangeListeners(allowResourceChangeListeners)
.setSafePaths(safePaths));
}
private PathResourceManager(Builder builder) {
this.allowResourceChangeListeners = builder.allowResourceChangeListeners;
if (builder.base == null) {
throw UndertowMessages.MESSAGES.argumentCannotBeNull("base");
}
this.fileSystem = builder.base.getFileSystem();
String basePath = null;
try {
if(builder.followLinks) {
basePath = builder.base.normalize().toRealPath().toString();
} else {
basePath = builder.base.normalize().toRealPath(LinkOption.NOFOLLOW_LINKS).toString();
}
} catch (IOException e) {
throw UndertowMessages.MESSAGES.failedToInitializePathManager(builder.base.toString(), e);
}
if (!basePath.endsWith(fileSystem.getSeparator())) {
basePath = basePath + fileSystem.getSeparator();
}
this.base = basePath;
this.transferMinSize = builder.transferMinSize;
this.caseSensitive = builder.caseSensitive;
this.followLinks = builder.followLinks;
if (this.followLinks) {
if (builder.safePaths == null) {
throw UndertowMessages.MESSAGES.argumentCannotBeNull("safePaths");
}
for (final String safePath : builder.safePaths) {
if (safePath == null) {
throw UndertowMessages.MESSAGES.argumentCannotBeNull("safePaths");
}
}
this.safePaths.addAll(Arrays.asList(builder.safePaths));
}
this.eTagFunction = builder.eTagFunction;
}
public Path getBasePath() {
return fileSystem.getPath(base);
}
public PathResourceManager setBase(final Path base) {
if (base == null) {
throw UndertowMessages.MESSAGES.argumentCannotBeNull("base");
}
this.fileSystem = base.getFileSystem();
String basePath = base.toAbsolutePath().toString();
if (!basePath.endsWith(fileSystem.getSeparator())) {
basePath = basePath + fileSystem.getSeparator();
}
this.base = basePath;
return this;
}
public PathResourceManager setBase(final File base) {
if (base == null) {
throw UndertowMessages.MESSAGES.argumentCannotBeNull("base");
}
this.fileSystem = FileSystems.getDefault();
String basePath = base.getAbsolutePath();
if (!basePath.endsWith(fileSystem.getSeparator())) {
basePath = basePath + fileSystem.getSeparator();
}
this.base = basePath;
return this;
}
public Resource getResource(final String p) {
if( p == null ) {
return null;
}
String path;
//base always ends with a /
if (p.startsWith("/")) {
path = p.substring(1);
} else {
path = p;
}
try {
Path file = fileSystem.getPath(base, path);
String normalizedFile = file.normalize().toString();
if(!normalizedFile.startsWith(base)) {
if(normalizedFile.length() == base.length() - 1) {
//special case for the root path, which may not have a trailing slash
if(!base.startsWith(normalizedFile)) {
log.tracef("Failed to get path resource %s from path resource manager with base %s, as file was outside the base directory", p, base);
return null;
}
} else {
log.tracef("Failed to get path resource %s from path resource manager with base %s, as file was outside the base directory", p, base);
return null;
}
}
if (Files.exists(file)) {
if(path.endsWith("/") && ! Files.isDirectory(file)) {
//UNDERTOW-432 don't return non directories if the path ends with a /
log.tracef("Failed to get path resource %s from path resource manager with base %s, as path ended with a / but was not a directory", p, base);
return null;
}
boolean followAll = this.followLinks && safePaths.isEmpty();
SymlinkResult symlinkBase = getSymlinkBase(base, file);
if (!followAll && symlinkBase != null && symlinkBase.requiresCheck) {
if (this.followLinks && isSymlinkSafe(file)) {
return getFileResource(file, path, symlinkBase.path, normalizedFile);
} else {
log.tracef("Failed to get path resource %s from path resource manager with base %s, as it was not a safe symlink path", p, base);
return null;
}
} else {
return getFileResource(file, path, symlinkBase == null ? null : symlinkBase.path, normalizedFile);
}
} else {
log.tracef("Failed to get path resource %s from path resource manager with base %s, as the path did not exist", p, base);
return null;
}
} catch (IOException e) {
UndertowLogger.REQUEST_LOGGER.debugf(e, "Invalid path %s", p);
return null;
} catch (SecurityException e) {
UndertowLogger.REQUEST_LOGGER.errorf(e, "Missing JSM permissions for path %s", p);
throw e;
} catch (Exception e) {
UndertowLogger.REQUEST_LOGGER.debugf(e, "Other issue for path %s", p);
return null;
}
}
@Override
public boolean isResourceChangeListenerSupported() {
return allowResourceChangeListeners;
}
@Override
public synchronized void registerResourceChangeListener(ResourceChangeListener listener) {
if(!allowResourceChangeListeners) {
//by rights we should throw an exception here, but this works around a bug in Wildfly where it just assumes
//PathResourceManager supports this. This will be fixed in a later version
return;
}
if (!fileSystem.equals(FileSystems.getDefault())) {
throw new IllegalStateException("Resource change listeners not supported when using a non-default file system");
}
listeners.add(listener);
if (fileSystemWatcher == null) {
fileSystemWatcher = Xnio.getInstance().createFileSystemWatcher("Watcher for " + base, OptionMap.EMPTY);
fileSystemWatcher.watchPath(new File(base), new FileChangeCallback() {
@Override
public void handleChanges(Collection changes) {
synchronized (PathResourceManager.this) {
final List events = new ArrayList<>();
for (FileChangeEvent change : changes) {
if (change.getFile().getAbsolutePath().startsWith(base)) {
String path = change.getFile().getAbsolutePath().substring(base.length());
if (File.separatorChar == '\\' && path.contains(File.separator)) {
path = path.replace(File.separatorChar, '/');
}
events.add(new ResourceChangeEvent(path, ResourceChangeEvent.Type.valueOf(change.getType().name())));
}
}
for (ResourceChangeListener listener : listeners) {
listener.handleChanges(events);
}
}
}
});
}
}
@Override
public synchronized void removeResourceChangeListener(ResourceChangeListener listener) {
if(!allowResourceChangeListeners) {
return;
}
listeners.remove(listener);
}
public long getTransferMinSize() {
return transferMinSize;
}
@Override
public synchronized void close() throws IOException {
if (fileSystemWatcher != null) {
fileSystemWatcher.close();
}
}
/**
* Returns true is some element of path inside base path is a symlink.
*/
private SymlinkResult getSymlinkBase(final String base, final Path file) {
int nameCount = file.getNameCount();
Path root = fileSystem.getPath(base);
int rootCount = root.getNameCount();
Path f = file;
for (int i = nameCount - 1; i>=0; i--) {
if (SecurityActions.isSymbolicLink(f)) {
return new SymlinkResult(i+1 > rootCount, f);
}
f = f.getParent();
}
return null;
}
/**
* Security check for case insensitive file systems.
* We make sure the case of the filename matches the case of the request.
* This is only a check for case sensitivity, not for non canonical . and ../ which are allowed.
*
* For example:
* file.getName() == "page.jsp" && file.getCanonicalFile().getName() == "page.jsp" should return true
* file.getName() == "page.jsp" && file.getCanonicalFile().getName() == "page.JSP" should return false
* file.getName() == "./page.jsp" && file.getCanonicalFile().getName() == "page.jsp" should return true
*/
private boolean isFileSameCase(final Path file, String normalizeFile) throws IOException {
String canonicalName = file.toRealPath().toString();
return canonicalName.equals(normalizeFile);
}
/**
* Security check for followSymlinks feature.
* Only follows those symbolink links defined in safePaths.
*/
private boolean isSymlinkSafe(final Path file) throws IOException {
String canonicalPath = file.toRealPath().toString();
for (String safePath : this.safePaths) {
if (safePath.length() > 0) {
if (safePath.startsWith(fileSystem.getSeparator())) {
/*
* Absolute path
*/
if (safePath.length() > 0 &&
canonicalPath.length() >= safePath.length() &&
canonicalPath.startsWith(safePath)) {
return true;
}
} else {
/*
* In relative path we build the path appending to base
*/
String absSafePath = base + fileSystem.getSeparator() + safePath;
Path absSafePathFile = fileSystem.getPath(absSafePath);
String canonicalSafePath = absSafePathFile.toRealPath().toString();
if (canonicalSafePath.length() > 0 &&
canonicalPath.length() >= canonicalSafePath.length() &&
canonicalPath.startsWith(canonicalSafePath)) {
return true;
}
}
}
}
return false;
}
/**
* Apply security check for case insensitive file systems.
*/
protected PathResource getFileResource(final Path file, final String path, final Path symlinkBase, String normalizedFile) throws IOException {
if (this.caseSensitive) {
if (symlinkBase != null) {
String relative = symlinkBase.relativize(file.normalize()).toString();
String fileResolved = file.toRealPath().toString();
String symlinkBaseResolved = symlinkBase.toRealPath().toString();
if (!fileResolved.startsWith(symlinkBaseResolved)) {
log.tracef("Rejected path resource %s from path resource manager with base %s, as the case did not match actual case of %s", path, base, normalizedFile);
return null;
}
String compare = fileResolved.substring(symlinkBaseResolved.length());
if(compare.startsWith(fileSystem.getSeparator())) {
compare = compare.substring(fileSystem.getSeparator().length());
}
if(relative.startsWith(fileSystem.getSeparator())) {
relative = relative.substring(fileSystem.getSeparator().length());
}
if (relative.equals(compare)) {
log.tracef("Found path resource %s from path resource manager with base %s", path, base);
return new PathResource(file, this, path, eTagFunction.generate(file));
}
log.tracef("Rejected path resource %s from path resource manager with base %s, as the case did not match actual case of %s", path, base, normalizedFile);
return null;
} else if (isFileSameCase(file, normalizedFile)) {
log.tracef("Found path resource %s from path resource manager with base %s", path, base);
return new PathResource(file, this, path, eTagFunction.generate(file));
} else {
log.tracef("Rejected path resource %s from path resource manager with base %s, as the case did not match actual case of %s", path, base, normalizedFile);
return null;
}
} else {
log.tracef("Found path resource %s from path resource manager with base %s", path, base);
return new PathResource(file, this, path, eTagFunction.generate(file));
}
}
private static class SymlinkResult {
public final boolean requiresCheck;
public final Path path;
private SymlinkResult(boolean requiresCheck, Path path) {
this.requiresCheck = requiresCheck;
this.path = path;
}
}
public interface ETagFunction {
/**
* Generates an {@link ETag} for the provided {@link Path}.
*
* @param path Path for which to generate an ETag
* @return ETag representing the provided path, or null
*/
ETag generate(Path path);
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private Path base;
private long transferMinSize = DEFAULT_TRANSFER_MIN_SIZE;
private boolean caseSensitive = true;
private boolean followLinks = false;
private boolean allowResourceChangeListeners = DEFAULT_CHANGE_LISTENERS_ALLOWED;
private ETagFunction eTagFunction = NULL_ETAG_FUNCTION;
private String[] safePaths;
private Builder() {
}
public Builder setBase(Path base) {
this.base = base;
return this;
}
public Builder setTransferMinSize(long transferMinSize) {
this.transferMinSize = transferMinSize;
return this;
}
public Builder setCaseSensitive(boolean caseSensitive) {
this.caseSensitive = caseSensitive;
return this;
}
public Builder setFollowLinks(boolean followLinks) {
this.followLinks = followLinks;
return this;
}
public Builder setAllowResourceChangeListeners(boolean allowResourceChangeListeners) {
this.allowResourceChangeListeners = allowResourceChangeListeners;
return this;
}
public Builder setETagFunction(ETagFunction eTagFunction) {
this.eTagFunction = eTagFunction;
return this;
}
public Builder setSafePaths(String[] safePaths) {
this.safePaths = safePaths;
return this;
}
public ResourceManager build() {
return new PathResourceManager(this);
}
}
}