blob: 1484b2eff9c5ed33f68552ba124f7868f8c844ec [file] [log] [blame]
/*
* 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 freemarker.cache;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import freemarker.log.Logger;
import freemarker.template.Configuration;
import freemarker.template.utility.SecurityUtilities;
import freemarker.template.utility.StringUtil;
/**
* A {@link TemplateLoader} that uses files inside a specified directory as the source of templates. By default it does
* security checks on the <em>canonical</em> path that will prevent it serving templates outside that specified
* directory. If you want symbolic links that point outside the template directory to work, you need to disable this
* feature by using {@link #FileTemplateLoader(File, boolean)} with {@code true} second argument, but before that, check
* the security implications there!
*/
public class FileTemplateLoader implements TemplateLoader {
/**
* By setting this Java system property to {@code true}, you can change the default of
* {@code #getEmulateCaseSensitiveFileSystem()}.
*/
public static String SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM
= "org.freemarker.emulateCaseSensitiveFileSystem";
private static final boolean EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT;
static {
final String s = SecurityUtilities.getSystemProperty(SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM,
"false");
boolean emuCaseSensFS;
try {
emuCaseSensFS = StringUtil.getYesNo(s);
} catch (Exception e) {
emuCaseSensFS = false;
}
EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT = emuCaseSensFS;
}
private static final int CASE_CHECH_CACHE_HARD_SIZE = 50;
private static final int CASE_CHECK_CACHE__SOFT_SIZE = 1000;
private static final boolean SEP_IS_SLASH = File.separatorChar == '/';
private static final Logger LOG = Logger.getLogger("freemarker.cache");
public final File baseDir;
private final String canonicalBasePath;
private boolean emulateCaseSensitiveFileSystem;
private MruCacheStorage correctCasePaths;
/**
* Creates a new file template cache that will use the current directory (the value of the system property
* <code>user.dir</code> as the base directory for loading templates. It will not allow access to template files
* that are accessible through symlinks that point outside the base directory.
*
* @deprecated Relying on what the current directory is is a bad practice; use
* {@link FileTemplateLoader#FileTemplateLoader(File)} instead.
*/
@Deprecated
public FileTemplateLoader() throws IOException {
this(new File(SecurityUtilities.getSystemProperty("user.dir")));
}
/**
* Creates a new file template loader that will use the specified directory
* as the base directory for loading templates. It will not allow access to
* template files that are accessible through symlinks that point outside
* the base directory.
* @param baseDir the base directory for loading templates
*/
public FileTemplateLoader(final File baseDir) throws IOException {
this(baseDir, false);
}
/**
* Creates a new file template loader that will use the specified directory as the base directory for loading
* templates. See the parameters for allowing symlinks that point outside the base directory.
*
* @param baseDir
* the base directory for loading templates
*
* @param disableCanonicalPathCheck
* If {@code true}, it will not check if the file to be loaded is inside the {@code baseDir} or not,
* according the <em>canonical</em> paths of the {@code baseDir} and the file to load. Note that
* {@link Configuration#getTemplate(String)} and (its overloads) already prevents backing out from the
* template directory with paths like {@code /../../../etc/password}, however, that can be circumvented
* with symbolic links or other file system features. If you really want to use symbolic links that point
* outside the {@code baseDir}, set this parameter to {@code true}, but then be very careful with
* template paths that are supplied by the visitor or an external system.
*/
public FileTemplateLoader(final File baseDir, final boolean disableCanonicalPathCheck) throws IOException {
try {
Object[] retval = AccessController.doPrivileged(new PrivilegedExceptionAction<Object[]>() {
public Object[] run() throws IOException {
if (!baseDir.exists()) {
throw new FileNotFoundException(baseDir + " does not exist.");
}
if (!baseDir.isDirectory()) {
throw new IOException(baseDir + " is not a directory.");
}
Object[] retval = new Object[2];
if (disableCanonicalPathCheck) {
retval[0] = baseDir;
retval[1] = null;
} else {
retval[0] = baseDir.getCanonicalFile();
String basePath = ((File) retval[0]).getPath();
// Most canonical paths don't end with File.separator,
// but some does. Like, "C:\" VS "C:\templates".
if (!basePath.endsWith(File.separator)) {
basePath += File.separatorChar;
}
retval[1] = basePath;
}
return retval;
}
});
this.baseDir = (File) retval[0];
this.canonicalBasePath = (String) retval[1];
setEmulateCaseSensitiveFileSystem(getEmulateCaseSensitiveFileSystemDefault());
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
public Object findTemplateSource(final String name) throws IOException {
try {
return AccessController.doPrivileged(new PrivilegedExceptionAction<File>() {
public File run() throws IOException {
File source = new File(baseDir, SEP_IS_SLASH ? name :
name.replace('/', File.separatorChar));
if (!source.isFile()) {
return null;
}
// Security check for inadvertently returning something
// outside the template directory when linking is not
// allowed.
if (canonicalBasePath != null) {
String normalized = source.getCanonicalPath();
if (!normalized.startsWith(canonicalBasePath)) {
throw new SecurityException(source.getAbsolutePath()
+ " resolves to " + normalized + " which "
+ " doesn't start with " + canonicalBasePath);
}
}
if (emulateCaseSensitiveFileSystem && !isNameCaseCorrect(source)) {
return null;
}
return source;
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
public long getLastModified(final Object templateSource) {
return (AccessController.doPrivileged(new PrivilegedAction<Long>() {
public Long run() {
return Long.valueOf(((File) templateSource).lastModified());
}
})).longValue();
}
public Reader getReader(final Object templateSource, final String encoding) throws IOException {
try {
return AccessController.doPrivileged(new PrivilegedExceptionAction<Reader>() {
public Reader run() throws IOException {
if (!(templateSource instanceof File)) {
throw new IllegalArgumentException(
"templateSource wasn't a File, but a: " +
templateSource.getClass().getName());
}
return new InputStreamReader(new FileInputStream((File) templateSource), encoding);
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
/**
* Called by {@link #findTemplateSource(String)} when {@link #getEmulateCaseSensitiveFileSystem()} is {@code true}.
*/
private boolean isNameCaseCorrect(File source) throws IOException {
final String sourcePath = source.getPath();
synchronized (correctCasePaths) {
if (correctCasePaths.get(sourcePath) != null) {
return true;
}
}
final File parentDir = source.getParentFile();
if (parentDir != null) {
if (!baseDir.equals(parentDir) && !isNameCaseCorrect(parentDir)) {
return false;
}
final String[] listing = parentDir.list();
if (listing != null) {
final String fileName = source.getName();
boolean identicalNameFound = false;
for (int i = 0; !identicalNameFound && i < listing.length; i++) {
if (fileName.equals(listing[i])) {
identicalNameFound = true;
}
}
if (!identicalNameFound) {
// If we find a similarly named file that only differs in case, then this is a file-not-found.
for (int i = 0; i < listing.length; i++) {
final String listingEntry = listing[i];
if (fileName.equalsIgnoreCase(listingEntry)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Emulating file-not-found because of letter case differences to the "
+ "real file, for: " + sourcePath);
}
return false;
}
}
}
}
}
synchronized (correctCasePaths) {
correctCasePaths.put(sourcePath, Boolean.TRUE);
}
return true;
}
public void closeTemplateSource(Object templateSource) {
// Do nothing.
}
/**
* Returns the base directory in which the templates are searched. This comes from the constructor argument, but
* it's possibly a canonicalized version of that.
*
* @since 2.3.21
*/
public File getBaseDirectory() {
return baseDir;
}
/**
* Intended for development only, checks if the template name matches the case (upper VS lower case letters) of the
* actual file name, and if it doesn't, it emulates a file-not-found even if the file system is case insensitive.
* This is useful when developing application on Windows, which will be later installed on Linux, OS X, etc. This
* check can be resource intensive, as to check the file name the directories involved, up to the
* {@link #getBaseDirectory()} directory, must be listed. Positive results (matching case) will be cached without
* expiration time.
*
* <p>The default in {@link FileTemplateLoader} is {@code false}, but subclasses may change they by overriding
* {@link #getEmulateCaseSensitiveFileSystemDefault()}.
*
* @since 2.3.23
*/
public void setEmulateCaseSensitiveFileSystem(boolean nameCaseChecked) {
// Ensure that the cache exists exactly when needed:
if (nameCaseChecked) {
if (correctCasePaths == null) {
correctCasePaths = new MruCacheStorage(CASE_CHECH_CACHE_HARD_SIZE, CASE_CHECK_CACHE__SOFT_SIZE);
}
} else {
correctCasePaths = null;
}
this.emulateCaseSensitiveFileSystem = nameCaseChecked;
}
/**
* Getter pair of {@link #setEmulateCaseSensitiveFileSystem(boolean)}.
*
* @since 2.3.23
*/
public boolean getEmulateCaseSensitiveFileSystem() {
return emulateCaseSensitiveFileSystem;
}
/**
* Returns the default of {@link #getEmulateCaseSensitiveFileSystem()}. In {@link FileTemplateLoader} it's
* {@code false}, unless the {@link #SYSTEM_PROPERTY_NAME_EMULATE_CASE_SENSITIVE_FILE_SYSTEM} system property was
* set to {@code true}, but this can be overridden here in custom subclasses. For example, if your environment
* defines something like developer mode, you may want to override this to return {@code true} on Windows.
*
* @since 2.3.23
*/
protected boolean getEmulateCaseSensitiveFileSystemDefault() {
return EMULATE_CASE_SENSITIVE_FILE_SYSTEM_DEFAULT;
}
/**
* Show class name and some details that are useful in template-not-found errors.
*
* @since 2.3.21
*/
@Override
public String toString() {
// We don't StringUtil.jQuote paths here, because on Windows there will be \\-s then that some may find
// confusing.
return TemplateLoaderUtils.getClassNameForToString(this) + "("
+ "baseDir=\"" + baseDir + "\""
+ (canonicalBasePath != null ? ", canonicalBasePath=\"" + canonicalBasePath + "\"" : "")
+ (emulateCaseSensitiveFileSystem ? ", emulateCaseSensitiveFileSystem=true" : "")
+ ")";
}
}