Merge pull request #8449 from eclipse/jetty-12.0.x-8426-aliaschecker-review
Issue #8426 - Review of alias checks for Jetty 12
This commit is contained in:
commit
3b8e1325cd
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// ========================================================================
|
||||||
|
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||||
|
//
|
||||||
|
// This program and the accompanying materials are made available under the
|
||||||
|
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||||
|
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||||
|
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||||
|
// ========================================================================
|
||||||
|
//
|
||||||
|
|
||||||
|
package org.eclipse.jetty.server;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||||
|
import org.eclipse.jetty.util.resource.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to check aliases.
|
||||||
|
*/
|
||||||
|
public interface AliasCheck
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Check if an alias is allowed to be served. If any {@link AliasCheck} returns
|
||||||
|
* true then the alias will be allowed to be served, therefore any alias checker
|
||||||
|
* should take things like the {@link ContextHandler#getProtectedTargets()} and
|
||||||
|
* Security Constraints into consideration before allowing a return a value of true.
|
||||||
|
*
|
||||||
|
* @param pathInContext The path the aliased resource was created for.
|
||||||
|
* @param resource The aliased resourced.
|
||||||
|
* @return True if the resource is OK to be served.
|
||||||
|
*/
|
||||||
|
boolean checkAlias(String pathInContext, Resource resource);
|
||||||
|
}
|
|
@ -36,7 +36,7 @@ import org.slf4j.LoggerFactory;
|
||||||
* <p>Aliases approved by this may still be able to bypass SecurityConstraints, so this class would need to be extended
|
* <p>Aliases approved by this may still be able to bypass SecurityConstraints, so this class would need to be extended
|
||||||
* to enforce any additional security constraints that are required.</p>
|
* to enforce any additional security constraints that are required.</p>
|
||||||
*/
|
*/
|
||||||
public class AllowedResourceAliasChecker extends AbstractLifeCycle implements ContextHandler.AliasCheck
|
public class AllowedResourceAliasChecker extends AbstractLifeCycle implements AliasCheck
|
||||||
{
|
{
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AllowedResourceAliasChecker.class);
|
private static final Logger LOG = LoggerFactory.getLogger(AllowedResourceAliasChecker.class);
|
||||||
protected static final LinkOption[] FOLLOW_LINKS = new LinkOption[0];
|
protected static final LinkOption[] FOLLOW_LINKS = new LinkOption[0];
|
||||||
|
@ -75,9 +75,7 @@ public class AllowedResourceAliasChecker extends AbstractLifeCycle implements Co
|
||||||
|
|
||||||
private String[] getProtectedTargets()
|
private String[] getProtectedTargets()
|
||||||
{
|
{
|
||||||
// TODO: Add protected targets to ContextHandler.
|
return _contextHandler.getProtectedTargets();
|
||||||
// return _contextHandler.getProtectedTargets();
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void extractBaseResourceFromContext()
|
private void extractBaseResourceFromContext()
|
||||||
|
@ -130,7 +128,7 @@ public class AllowedResourceAliasChecker extends AbstractLifeCycle implements Co
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean check(String pathInContext, Resource resource)
|
public boolean checkAlias(String pathInContext, Resource resource)
|
||||||
{
|
{
|
||||||
if (!_initialized)
|
if (!_initialized)
|
||||||
extractBaseResourceFromContext();
|
extractBaseResourceFromContext();
|
||||||
|
|
|
@ -39,6 +39,7 @@ import org.eclipse.jetty.http.QuotedCSV;
|
||||||
import org.eclipse.jetty.http.QuotedQualityCSV;
|
import org.eclipse.jetty.http.QuotedQualityCSV;
|
||||||
import org.eclipse.jetty.http.ResourceHttpContent;
|
import org.eclipse.jetty.http.ResourceHttpContent;
|
||||||
import org.eclipse.jetty.io.Content;
|
import org.eclipse.jetty.io.Content;
|
||||||
|
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||||
import org.eclipse.jetty.util.BufferUtil;
|
import org.eclipse.jetty.util.BufferUtil;
|
||||||
import org.eclipse.jetty.util.Callback;
|
import org.eclipse.jetty.util.Callback;
|
||||||
import org.eclipse.jetty.util.IO;
|
import org.eclipse.jetty.util.IO;
|
||||||
|
@ -95,11 +96,24 @@ public class ResourceService
|
||||||
return _stylesheet;
|
return _stylesheet;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpContent getContent(String path) throws IOException
|
public HttpContent getContent(String path, Request request) throws IOException
|
||||||
|
{
|
||||||
|
ContextHandler contextHandler = ContextHandler.getContextHandler(request);
|
||||||
|
return getContent(path, contextHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpContent getContent(String path, AliasCheck aliasCheck) throws IOException
|
||||||
{
|
{
|
||||||
HttpContent content = _contentFactory.getContent(path == null ? "" : path);
|
HttpContent content = _contentFactory.getContent(path == null ? "" : path);
|
||||||
if ((content == null) && (_stylesheet != null) && (path != null) && path.endsWith("/jetty-dir.css"))
|
if (content != null)
|
||||||
content = new ResourceHttpContent(_stylesheet, "text/css");
|
{
|
||||||
|
if (aliasCheck != null && !aliasCheck.checkAlias(path, content.getResource()))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if ((_stylesheet != null) && (path != null) && path.endsWith("/jetty-dir.css"))
|
||||||
|
content = new ResourceHttpContent(_stylesheet, "text/css");
|
||||||
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under the
|
|
||||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
|
||||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package org.eclipse.jetty.server.handler;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.server.Handler;
|
|
||||||
import org.eclipse.jetty.server.handler.ContextHandler.AliasCheck;
|
|
||||||
import org.eclipse.jetty.util.resource.PathResource;
|
|
||||||
import org.eclipse.jetty.util.resource.Resource;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Symbolic Link AliasChecker.
|
|
||||||
* <p>An instance of this class can be registered with {@link Handler} subclasses
|
|
||||||
* to check resources that are aliased to other locations.</p>
|
|
||||||
* <p>The checker uses the Java {@link Files#readSymbolicLink(Path)} and
|
|
||||||
* {@link Path#toRealPath(java.nio.file.LinkOption...)}
|
|
||||||
* APIs to check if a file is aliased with symbolic links.</p>
|
|
||||||
* @deprecated use {@link org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker} instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public class AllowSymLinkAliasChecker implements AliasCheck
|
|
||||||
{
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AllowSymLinkAliasChecker.class);
|
|
||||||
|
|
||||||
public AllowSymLinkAliasChecker()
|
|
||||||
{
|
|
||||||
LOG.warn("Deprecated, use SymlinkAllowedResourceAliasChecker instead.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean check(String pathInContext, Resource resource)
|
|
||||||
{
|
|
||||||
// Only support PathResource alias checking
|
|
||||||
if (!(resource instanceof PathResource pathResource))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Path path = pathResource.getPath();
|
|
||||||
Path alias = pathResource.getAliasPath();
|
|
||||||
|
|
||||||
if (PathResource.isSameName(alias, path))
|
|
||||||
return false; // Unknown why this is an alias
|
|
||||||
|
|
||||||
if (hasSymbolicLink(path) && Files.isSameFile(path, alias))
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Allow symlink {} --> {}", resource, pathResource.getAliasPath());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LOG.trace("IGNORED", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasSymbolicLink(Path path)
|
|
||||||
{
|
|
||||||
// Is file itself a symlink?
|
|
||||||
if (Files.isSymbolicLink(path))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lets try each path segment
|
|
||||||
Path base = path.getRoot();
|
|
||||||
for (Path segment : path)
|
|
||||||
{
|
|
||||||
base = base.resolve(segment);
|
|
||||||
if (Files.isSymbolicLink(base))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,6 +20,7 @@ import java.net.URLClassLoader;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.EventListener;
|
import java.util.EventListener;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -33,14 +34,17 @@ import java.util.stream.Collectors;
|
||||||
import org.eclipse.jetty.http.HttpField;
|
import org.eclipse.jetty.http.HttpField;
|
||||||
import org.eclipse.jetty.http.HttpHeader;
|
import org.eclipse.jetty.http.HttpHeader;
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
|
import org.eclipse.jetty.server.AliasCheck;
|
||||||
import org.eclipse.jetty.server.Connector;
|
import org.eclipse.jetty.server.Connector;
|
||||||
import org.eclipse.jetty.server.Handler;
|
import org.eclipse.jetty.server.Handler;
|
||||||
import org.eclipse.jetty.server.Request;
|
import org.eclipse.jetty.server.Request;
|
||||||
import org.eclipse.jetty.server.Response;
|
import org.eclipse.jetty.server.Response;
|
||||||
import org.eclipse.jetty.server.Server;
|
import org.eclipse.jetty.server.Server;
|
||||||
|
import org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker;
|
||||||
import org.eclipse.jetty.util.Attributes;
|
import org.eclipse.jetty.util.Attributes;
|
||||||
import org.eclipse.jetty.util.Callback;
|
import org.eclipse.jetty.util.Callback;
|
||||||
import org.eclipse.jetty.util.DecoratedObjectFactory;
|
import org.eclipse.jetty.util.DecoratedObjectFactory;
|
||||||
|
import org.eclipse.jetty.util.Index;
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
import org.eclipse.jetty.util.StringUtil;
|
||||||
import org.eclipse.jetty.util.TypeUtil;
|
import org.eclipse.jetty.util.TypeUtil;
|
||||||
import org.eclipse.jetty.util.URIUtil;
|
import org.eclipse.jetty.util.URIUtil;
|
||||||
|
@ -48,13 +52,14 @@ import org.eclipse.jetty.util.annotation.ManagedAttribute;
|
||||||
import org.eclipse.jetty.util.component.ClassLoaderDump;
|
import org.eclipse.jetty.util.component.ClassLoaderDump;
|
||||||
import org.eclipse.jetty.util.component.Dumpable;
|
import org.eclipse.jetty.util.component.Dumpable;
|
||||||
import org.eclipse.jetty.util.component.Graceful;
|
import org.eclipse.jetty.util.component.Graceful;
|
||||||
|
import org.eclipse.jetty.util.component.LifeCycle;
|
||||||
import org.eclipse.jetty.util.resource.Resource;
|
import org.eclipse.jetty.util.resource.Resource;
|
||||||
import org.eclipse.jetty.util.resource.ResourceFactory;
|
import org.eclipse.jetty.util.resource.ResourceFactory;
|
||||||
import org.eclipse.jetty.util.thread.Invocable;
|
import org.eclipse.jetty.util.thread.Invocable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class ContextHandler extends Handler.Wrapper implements Attributes, Graceful
|
public class ContextHandler extends Handler.Wrapper implements Attributes, Graceful, AliasCheck
|
||||||
{
|
{
|
||||||
// TODO where should the alias checking go?
|
// TODO where should the alias checking go?
|
||||||
// TODO add protected paths to ServletContextHandler?
|
// TODO add protected paths to ServletContextHandler?
|
||||||
|
@ -92,6 +97,8 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
|
||||||
private ClassLoader _classLoader;
|
private ClassLoader _classLoader;
|
||||||
private Request.Processor _errorProcessor;
|
private Request.Processor _errorProcessor;
|
||||||
private boolean _allowNullPathInContext;
|
private boolean _allowNullPathInContext;
|
||||||
|
private Index<ProtectedTargetType> _protectedTargets = Index.empty(false);
|
||||||
|
private final List<AliasCheck> _aliasChecks = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
public enum Availability
|
public enum Availability
|
||||||
{
|
{
|
||||||
|
@ -102,6 +109,22 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
|
||||||
SHUTDOWN, // graceful shutdown
|
SHUTDOWN, // graceful shutdown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of protected target match
|
||||||
|
* @see #_protectedTargets
|
||||||
|
*/
|
||||||
|
private enum ProtectedTargetType
|
||||||
|
{
|
||||||
|
EXACT,
|
||||||
|
PREFIX
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ContextHandler getContextHandler(Request request)
|
||||||
|
{
|
||||||
|
ContextRequest contextRequest = Request.as(request, ContextRequest.class);
|
||||||
|
return (contextRequest == null) ? null : contextRequest.getContext().getContextHandler();
|
||||||
|
}
|
||||||
|
|
||||||
private final AtomicReference<Availability> _availability = new AtomicReference<>(Availability.STOPPED);
|
private final AtomicReference<Availability> _availability = new AtomicReference<>(Availability.STOPPED);
|
||||||
|
|
||||||
public ContextHandler()
|
public ContextHandler()
|
||||||
|
@ -122,6 +145,9 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
|
||||||
setContextPath(contextPath);
|
setContextPath(contextPath);
|
||||||
if (parent != null)
|
if (parent != null)
|
||||||
parent.addHandler(this);
|
parent.addHandler(this);
|
||||||
|
|
||||||
|
if (File.separatorChar == '/')
|
||||||
|
addAliasCheck(new SymlinkAllowedResourceAliasChecker(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Context newContext()
|
protected Context newContext()
|
||||||
|
@ -728,6 +754,124 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
|
||||||
_persistentAttributes.clearAttributes();
|
_persistentAttributes.clearAttributes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the target when a target within a context is determined. If
|
||||||
|
* the target is protected, 404 is returned.
|
||||||
|
*
|
||||||
|
* @param target the target to test
|
||||||
|
* @return true if target is a protected target
|
||||||
|
*/
|
||||||
|
public boolean isProtectedTarget(String target)
|
||||||
|
{
|
||||||
|
if (target == null || _protectedTargets.isEmpty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (target.startsWith("//"))
|
||||||
|
target = URIUtil.compactPath(target);
|
||||||
|
|
||||||
|
ProtectedTargetType type = _protectedTargets.getBest(target);
|
||||||
|
|
||||||
|
return type == ProtectedTargetType.PREFIX ||
|
||||||
|
type == ProtectedTargetType.EXACT && _protectedTargets.get(target) == ProtectedTargetType.EXACT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param targets Array of URL prefix. Each prefix is in the form /path and will match either /path exactly or /path/anything
|
||||||
|
*/
|
||||||
|
public void setProtectedTargets(String[] targets)
|
||||||
|
{
|
||||||
|
Index.Builder<ProtectedTargetType> builder = new Index.Builder<>();
|
||||||
|
if (targets != null)
|
||||||
|
{
|
||||||
|
for (String t : targets)
|
||||||
|
{
|
||||||
|
if (!t.startsWith("/"))
|
||||||
|
throw new IllegalArgumentException("Bad protected target: " + t);
|
||||||
|
|
||||||
|
builder.with(t, ProtectedTargetType.EXACT);
|
||||||
|
builder.with(t + "/", ProtectedTargetType.PREFIX);
|
||||||
|
builder.with(t + "?", ProtectedTargetType.PREFIX);
|
||||||
|
builder.with(t + "#", ProtectedTargetType.PREFIX);
|
||||||
|
builder.with(t + ";", ProtectedTargetType.PREFIX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_protectedTargets = builder.caseSensitive(false).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getProtectedTargets()
|
||||||
|
{
|
||||||
|
if (_protectedTargets == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return _protectedTargets.keySet().stream()
|
||||||
|
.filter(s -> _protectedTargets.get(s) == ProtectedTargetType.EXACT)
|
||||||
|
.toArray(String[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an AliasCheck instance to possibly permit aliased resources
|
||||||
|
*
|
||||||
|
* @param check The alias checker
|
||||||
|
*/
|
||||||
|
public void addAliasCheck(AliasCheck check)
|
||||||
|
{
|
||||||
|
_aliasChecks.add(check);
|
||||||
|
if (check instanceof LifeCycle)
|
||||||
|
addManaged((LifeCycle)check);
|
||||||
|
else
|
||||||
|
addBean(check);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Immutable list of Alias checks
|
||||||
|
*/
|
||||||
|
public List<AliasCheck> getAliasChecks()
|
||||||
|
{
|
||||||
|
return Collections.unmodifiableList(_aliasChecks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param checks list of AliasCheck instances
|
||||||
|
*/
|
||||||
|
public void setAliasChecks(List<AliasCheck> checks)
|
||||||
|
{
|
||||||
|
clearAliasChecks();
|
||||||
|
checks.forEach(this::addAliasCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clear the list of AliasChecks
|
||||||
|
*/
|
||||||
|
public void clearAliasChecks()
|
||||||
|
{
|
||||||
|
_aliasChecks.forEach(this::removeBean);
|
||||||
|
_aliasChecks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkAlias(String pathInContext, Resource resource)
|
||||||
|
{
|
||||||
|
// Is the resource aliased?
|
||||||
|
if (resource.isAlias())
|
||||||
|
{
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
LOG.debug("Aliased resource: {}~={}", resource, resource.getAlias());
|
||||||
|
|
||||||
|
// alias checks
|
||||||
|
for (AliasCheck check : _aliasChecks)
|
||||||
|
{
|
||||||
|
if (check.checkAlias(pathInContext, resource))
|
||||||
|
{
|
||||||
|
if (LOG.isDebugEnabled())
|
||||||
|
LOG.debug("Aliased resource: {} approved by {}", resource, check);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
|
@ -987,22 +1131,6 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface to check aliases
|
|
||||||
* TODO REVIEW!!!
|
|
||||||
*/
|
|
||||||
public interface AliasCheck
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Check an alias
|
|
||||||
*
|
|
||||||
* @param pathInContext The path the aliased resource was created for
|
|
||||||
* @param resource The aliased resourced
|
|
||||||
* @return True if the resource is OK to be served.
|
|
||||||
*/
|
|
||||||
boolean check(String pathInContext, Resource resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener for all threads entering context scope, including async IO callbacks
|
* Listener for all threads entering context scope, including async IO callbacks
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -110,7 +110,7 @@ public class ContextRequest extends Request.WrapperProcessor implements Invocabl
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public org.eclipse.jetty.server.Context getContext()
|
public ContextHandler.Context getContext()
|
||||||
{
|
{
|
||||||
return _context;
|
return _context;
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,7 @@ public class ResourceHandler extends Handler.Wrapper
|
||||||
return super.handle(request);
|
return super.handle(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpContent content = _resourceService.getContent(request.getPathInContext());
|
HttpContent content = _resourceService.getContent(request.getPathInContext(), request);
|
||||||
if (content == null)
|
if (content == null)
|
||||||
return super.handle(request); // no content - try other handlers
|
return super.handle(request); // no content - try other handlers
|
||||||
|
|
||||||
|
|
|
@ -675,12 +675,12 @@ public class ResourceHandlerTest
|
||||||
_server.addConnector(_local);
|
_server.addConnector(_local);
|
||||||
|
|
||||||
_rootResourceHandler = new ResourceHandler();
|
_rootResourceHandler = new ResourceHandler();
|
||||||
_rootResourceHandler.setBaseResource(ResourceFactory.root().newResource(docRoot));
|
|
||||||
_rootResourceHandler.setWelcomeFiles("welcome.txt");
|
_rootResourceHandler.setWelcomeFiles("welcome.txt");
|
||||||
_rootResourceHandler.setRedirectWelcome(false);
|
_rootResourceHandler.setRedirectWelcome(false);
|
||||||
|
|
||||||
ContextHandler contextHandler = new ContextHandler("/context");
|
ContextHandler contextHandler = new ContextHandler("/context");
|
||||||
contextHandler.setHandler(_rootResourceHandler);
|
contextHandler.setHandler(_rootResourceHandler);
|
||||||
|
contextHandler.setBaseResource(ResourceFactory.root().newResource(docRoot));
|
||||||
|
|
||||||
_contextHandlerCollection = new ContextHandlerCollection();
|
_contextHandlerCollection = new ContextHandlerCollection();
|
||||||
_contextHandlerCollection.addHandler(contextHandler);
|
_contextHandlerCollection.addHandler(contextHandler);
|
||||||
|
|
|
@ -338,7 +338,7 @@ public class DefaultServlet extends HttpServlet
|
||||||
boolean included = req.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
|
boolean included = req.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
HttpContent content = _resourceService.getContent(pathInContext);
|
HttpContent content = _resourceService.getContent(pathInContext, ServletContextRequest.getBaseRequest(req));
|
||||||
if (content == null || !content.getResource().exists())
|
if (content == null || !content.getResource().exists())
|
||||||
{
|
{
|
||||||
if (included)
|
if (included)
|
||||||
|
@ -918,20 +918,6 @@ public class DefaultServlet extends HttpServlet
|
||||||
_servletContextHandler = servletContextHandler;
|
_servletContextHandler = servletContextHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public HttpContent getContent(String path) throws IOException
|
|
||||||
{
|
|
||||||
HttpContent httpContent = super.getContent(path);
|
|
||||||
|
|
||||||
if (httpContent != null)
|
|
||||||
{
|
|
||||||
if (!_servletContextHandler.checkAlias(path, httpContent.getResource()))
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return httpContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getWelcomeTarget(Request coreRequest) throws IOException
|
public String getWelcomeTarget(Request coreRequest) throws IOException
|
||||||
{
|
{
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
|
|
||||||
package org.eclipse.jetty.ee10.servlet;
|
package org.eclipse.jetty.ee10.servlet;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
|
@ -78,7 +77,6 @@ import org.eclipse.jetty.http.MimeTypes;
|
||||||
import org.eclipse.jetty.server.Handler;
|
import org.eclipse.jetty.server.Handler;
|
||||||
import org.eclipse.jetty.server.Request;
|
import org.eclipse.jetty.server.Request;
|
||||||
import org.eclipse.jetty.server.Response;
|
import org.eclipse.jetty.server.Response;
|
||||||
import org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker;
|
|
||||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||||
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
|
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
|
||||||
import org.eclipse.jetty.server.handler.ContextRequest;
|
import org.eclipse.jetty.server.handler.ContextRequest;
|
||||||
|
@ -86,7 +84,6 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
||||||
import org.eclipse.jetty.util.DecoratedObjectFactory;
|
import org.eclipse.jetty.util.DecoratedObjectFactory;
|
||||||
import org.eclipse.jetty.util.DeprecationWarning;
|
import org.eclipse.jetty.util.DeprecationWarning;
|
||||||
import org.eclipse.jetty.util.ExceptionUtil;
|
import org.eclipse.jetty.util.ExceptionUtil;
|
||||||
import org.eclipse.jetty.util.Index;
|
|
||||||
import org.eclipse.jetty.util.Loader;
|
import org.eclipse.jetty.util.Loader;
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
import org.eclipse.jetty.util.StringUtil;
|
||||||
import org.eclipse.jetty.util.URIUtil;
|
import org.eclipse.jetty.util.URIUtil;
|
||||||
|
@ -151,16 +148,6 @@ public class ServletContextHandler extends ContextHandler implements Graceful
|
||||||
DESTROYED
|
DESTROYED
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of protected target match
|
|
||||||
* @see #_protectedTargets
|
|
||||||
*/
|
|
||||||
protected enum ProtectedTargetType
|
|
||||||
{
|
|
||||||
EXACT,
|
|
||||||
PREFIX
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ServletContextHandler getServletContextHandler(ServletContext servletContext, String purpose)
|
public static ServletContextHandler getServletContextHandler(ServletContext servletContext, String purpose)
|
||||||
{
|
{
|
||||||
if (servletContext instanceof ServletContextApi servletContextApi)
|
if (servletContext instanceof ServletContextApi servletContextApi)
|
||||||
|
@ -221,8 +208,6 @@ public class ServletContextHandler extends ContextHandler implements Graceful
|
||||||
private final List<ServletRequestAttributeListener> _servletRequestAttributeListeners = new CopyOnWriteArrayList<>();
|
private final List<ServletRequestAttributeListener> _servletRequestAttributeListeners = new CopyOnWriteArrayList<>();
|
||||||
private final List<ServletContextScopeListener> _contextListeners = new CopyOnWriteArrayList<>();
|
private final List<ServletContextScopeListener> _contextListeners = new CopyOnWriteArrayList<>();
|
||||||
private final Set<EventListener> _durableListeners = new HashSet<>();
|
private final Set<EventListener> _durableListeners = new HashSet<>();
|
||||||
private Index<ProtectedTargetType> _protectedTargets = Index.empty(false);
|
|
||||||
private final List<AliasCheck> _aliasChecks = new CopyOnWriteArrayList<>();
|
|
||||||
|
|
||||||
protected final DecoratedObjectFactory _objFactory;
|
protected final DecoratedObjectFactory _objFactory;
|
||||||
// protected Class<? extends SecurityHandler> _defaultSecurityHandlerClass = org.eclipse.jetty.security.ConstraintSecurityHandler.class;
|
// protected Class<? extends SecurityHandler> _defaultSecurityHandlerClass = org.eclipse.jetty.security.ConstraintSecurityHandler.class;
|
||||||
|
@ -277,9 +262,6 @@ public class ServletContextHandler extends ContextHandler implements Graceful
|
||||||
public ServletContextHandler(Container parent, String contextPath, SessionHandler sessionHandler, SecurityHandler securityHandler, ServletHandler servletHandler, ErrorHandler errorHandler, int options)
|
public ServletContextHandler(Container parent, String contextPath, SessionHandler sessionHandler, SecurityHandler securityHandler, ServletHandler servletHandler, ErrorHandler errorHandler, int options)
|
||||||
{
|
{
|
||||||
_servletContext = newServletContextApi();
|
_servletContext = newServletContextApi();
|
||||||
|
|
||||||
if (File.separatorChar == '/')
|
|
||||||
addAliasCheck(new SymlinkAllowedResourceAliasChecker(this));
|
|
||||||
|
|
||||||
if (contextPath != null)
|
if (contextPath != null)
|
||||||
setContextPath(contextPath);
|
setContextPath(contextPath);
|
||||||
|
@ -603,60 +585,6 @@ public class ServletContextHandler extends ContextHandler implements Graceful
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check the target. Called by {@link #handle(Request)} when a target within a context is determined. If
|
|
||||||
* the target is protected, 404 is returned.
|
|
||||||
*
|
|
||||||
* @param target the target to test
|
|
||||||
* @return true if target is a protected target
|
|
||||||
*/
|
|
||||||
public boolean isProtectedTarget(String target)
|
|
||||||
{
|
|
||||||
if (target == null || _protectedTargets.isEmpty())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (target.startsWith("//"))
|
|
||||||
target = URIUtil.compactPath(target);
|
|
||||||
|
|
||||||
ProtectedTargetType type = _protectedTargets.getBest(target);
|
|
||||||
|
|
||||||
return type == ProtectedTargetType.PREFIX ||
|
|
||||||
type == ProtectedTargetType.EXACT && _protectedTargets.get(target) == ProtectedTargetType.EXACT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param targets Array of URL prefix. Each prefix is in the form /path and will match either /path exactly or /path/anything
|
|
||||||
*/
|
|
||||||
public void setProtectedTargets(String[] targets)
|
|
||||||
{
|
|
||||||
Index.Builder<ProtectedTargetType> builder = new Index.Builder<>();
|
|
||||||
if (targets != null)
|
|
||||||
{
|
|
||||||
for (String t : targets)
|
|
||||||
{
|
|
||||||
if (!t.startsWith("/"))
|
|
||||||
throw new IllegalArgumentException("Bad protected target: " + t);
|
|
||||||
|
|
||||||
builder.with(t, ProtectedTargetType.EXACT);
|
|
||||||
builder.with(t + "/", ProtectedTargetType.PREFIX);
|
|
||||||
builder.with(t + "?", ProtectedTargetType.PREFIX);
|
|
||||||
builder.with(t + "#", ProtectedTargetType.PREFIX);
|
|
||||||
builder.with(t + ";", ProtectedTargetType.PREFIX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_protectedTargets = builder.caseSensitive(false).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String[] getProtectedTargets()
|
|
||||||
{
|
|
||||||
if (_protectedTargets == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return _protectedTargets.keySet().stream()
|
|
||||||
.filter(s -> _protectedTargets.get(s) == ProtectedTargetType.EXACT)
|
|
||||||
.toArray(String[]::new);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDefaultRequestCharacterEncoding(String encoding)
|
public void setDefaultRequestCharacterEncoding(String encoding)
|
||||||
{
|
{
|
||||||
_defaultRequestCharacterEncoding = encoding;
|
_defaultRequestCharacterEncoding = encoding;
|
||||||
|
@ -857,51 +785,7 @@ public class ServletContextHandler extends ContextHandler implements Graceful
|
||||||
if (baseResource == null)
|
if (baseResource == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
try
|
return baseResource.resolve(pathInContext);
|
||||||
{
|
|
||||||
// addPath with accept non-canonical paths that don't go above the root,
|
|
||||||
// but will treat them as aliases. So unless allowed by an AliasChecker
|
|
||||||
// they will be rejected below.
|
|
||||||
Resource resource = baseResource.resolve(pathInContext);
|
|
||||||
|
|
||||||
if (checkAlias(pathInContext, resource))
|
|
||||||
return resource;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LOG.trace("IGNORED", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param path the path to check the alias for
|
|
||||||
* @param resource the resource
|
|
||||||
* @return True if the alias is OK
|
|
||||||
*/
|
|
||||||
public boolean checkAlias(String path, Resource resource)
|
|
||||||
{
|
|
||||||
// Is the resource aliased?
|
|
||||||
if (resource.isAlias())
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Aliased resource: {}~={}", resource, resource.getAlias());
|
|
||||||
|
|
||||||
// alias checks
|
|
||||||
for (AliasCheck check : _aliasChecks)
|
|
||||||
{
|
|
||||||
if (check.check(path, resource))
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Aliased resource: {} approved by {}", resource, check);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -990,46 +874,6 @@ public class ServletContextHandler extends ContextHandler implements Graceful
|
||||||
return host;
|
return host;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add an AliasCheck instance to possibly permit aliased resources
|
|
||||||
*
|
|
||||||
* @param check The alias checker
|
|
||||||
*/
|
|
||||||
public void addAliasCheck(AliasCheck check)
|
|
||||||
{
|
|
||||||
_aliasChecks.add(check);
|
|
||||||
if (check instanceof LifeCycle)
|
|
||||||
addManaged((LifeCycle)check);
|
|
||||||
else
|
|
||||||
addBean(check);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Immutable list of Alias checks
|
|
||||||
*/
|
|
||||||
public List<AliasCheck> getAliasChecks()
|
|
||||||
{
|
|
||||||
return Collections.unmodifiableList(_aliasChecks);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param checks list of AliasCheck instances
|
|
||||||
*/
|
|
||||||
public void setAliasChecks(List<AliasCheck> checks)
|
|
||||||
{
|
|
||||||
clearAliasChecks();
|
|
||||||
checks.forEach(this::addAliasCheck);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* clear the list of AliasChecks
|
|
||||||
*/
|
|
||||||
public void clearAliasChecks()
|
|
||||||
{
|
|
||||||
_aliasChecks.forEach(this::removeBean);
|
|
||||||
_aliasChecks.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener for all threads entering context scope, including async IO callbacks
|
* Listener for all threads entering context scope, including async IO callbacks
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -25,7 +25,6 @@ import org.eclipse.jetty.http.HttpStatus;
|
||||||
import org.eclipse.jetty.server.Connector;
|
import org.eclipse.jetty.server.Connector;
|
||||||
import org.eclipse.jetty.server.LocalConnector;
|
import org.eclipse.jetty.server.LocalConnector;
|
||||||
import org.eclipse.jetty.server.Server;
|
import org.eclipse.jetty.server.Server;
|
||||||
import org.eclipse.jetty.server.handler.AllowSymLinkAliasChecker;
|
|
||||||
import org.eclipse.jetty.server.handler.DefaultHandler;
|
import org.eclipse.jetty.server.handler.DefaultHandler;
|
||||||
import org.eclipse.jetty.server.handler.HandlerList;
|
import org.eclipse.jetty.server.handler.HandlerList;
|
||||||
import org.eclipse.jetty.server.handler.ResourceHandler;
|
import org.eclipse.jetty.server.handler.ResourceHandler;
|
||||||
|
@ -78,8 +77,6 @@ public class AliasedConstraintTest
|
||||||
|
|
||||||
server.setHandler(new HandlerList(context, new DefaultHandler()));
|
server.setHandler(new HandlerList(context, new DefaultHandler()));
|
||||||
|
|
||||||
context.addAliasCheck(new AllowSymLinkAliasChecker());
|
|
||||||
|
|
||||||
server.addBean(loginService);
|
server.addBean(loginService);
|
||||||
|
|
||||||
security = new ConstraintSecurityHandler();
|
security = new ConstraintSecurityHandler();
|
||||||
|
|
|
@ -1,159 +0,0 @@
|
||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under the
|
|
||||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
|
||||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package org.eclipse.jetty.test;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletContextEvent;
|
|
||||||
import jakarta.servlet.ServletContextListener;
|
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
|
||||||
import org.eclipse.jetty.client.api.ContentResponse;
|
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
|
||||||
import org.eclipse.jetty.server.Server;
|
|
||||||
import org.eclipse.jetty.server.ServerConnector;
|
|
||||||
import org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker;
|
|
||||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
|
||||||
import org.eclipse.jetty.servlet.DefaultServlet;
|
|
||||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
|
||||||
import org.eclipse.jetty.util.IO;
|
|
||||||
import org.eclipse.jetty.util.resource.PathResource;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
|
||||||
import static org.hamcrest.Matchers.containsString;
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
|
||||||
import static org.hamcrest.Matchers.is;
|
|
||||||
import static org.hamcrest.Matchers.not;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
|
|
||||||
public class AliasCheckerWebRootIsSymlinkTest
|
|
||||||
{
|
|
||||||
private Server _server;
|
|
||||||
private ServerConnector _connector;
|
|
||||||
private HttpClient _client;
|
|
||||||
private ServletContextHandler _context;
|
|
||||||
private Path _webrootSymlink;
|
|
||||||
|
|
||||||
private static Path getResource(String path) throws Exception
|
|
||||||
{
|
|
||||||
URL url = AliasCheckerWebRootIsSymlinkTest.class.getClassLoader().getResource(path);
|
|
||||||
assertNotNull(url);
|
|
||||||
return new File(url.toURI()).toPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void delete(Path path)
|
|
||||||
{
|
|
||||||
IO.delete(path.toFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setAliasChecker(ContextHandler.AliasCheck aliasChecker)
|
|
||||||
{
|
|
||||||
_context.clearAliasChecks();
|
|
||||||
if (aliasChecker != null)
|
|
||||||
_context.addAliasCheck(aliasChecker);
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void before() throws Exception
|
|
||||||
{
|
|
||||||
Path webRootPath = getResource("webroot");
|
|
||||||
|
|
||||||
// External symlink to webroot.
|
|
||||||
_webrootSymlink = webRootPath.resolve("../webrootSymlink");
|
|
||||||
delete(_webrootSymlink);
|
|
||||||
Files.createSymbolicLink(_webrootSymlink, webRootPath).toFile().deleteOnExit();
|
|
||||||
|
|
||||||
// Create and start Server and Client.
|
|
||||||
_server = new Server();
|
|
||||||
_connector = new ServerConnector(_server);
|
|
||||||
_server.addConnector(_connector);
|
|
||||||
_context = new ServletContextHandler();
|
|
||||||
|
|
||||||
_context.setContextPath("/");
|
|
||||||
_context.setBaseResource(new PathResource(_webrootSymlink));
|
|
||||||
_context.setWelcomeFiles(new String[]{"index.html"});
|
|
||||||
_context.getMimeTypes().addMimeMapping("txt", "text/plain;charset=utf-8");
|
|
||||||
_server.setHandler(_context);
|
|
||||||
_context.addServlet(DefaultServlet.class, "/");
|
|
||||||
_context.clearAliasChecks();
|
|
||||||
|
|
||||||
_client = new HttpClient();
|
|
||||||
_client.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
public void after() throws Exception
|
|
||||||
{
|
|
||||||
// Try to delete all files now so that the symlinks do not confuse other tests.
|
|
||||||
Files.delete(_webrootSymlink);
|
|
||||||
|
|
||||||
_client.stop();
|
|
||||||
_server.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void test() throws Exception
|
|
||||||
{
|
|
||||||
// WEB-INF is a protected target, and enable symlink alias checker.
|
|
||||||
_context.setProtectedTargets(new String[]{"/WEB-INF"});
|
|
||||||
setAliasChecker(new SymlinkAllowedResourceAliasChecker(_context));
|
|
||||||
|
|
||||||
CompletableFuture<InputStream> resource = new CompletableFuture<>();
|
|
||||||
_context.addEventListener(new ServletContextListener()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void contextInitialized(ServletContextEvent sce)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Getting resource with API should allow you to bypass security constraints.
|
|
||||||
resource.complete(sce.getServletContext().getResourceAsStream("/WEB-INF/web.xml"));
|
|
||||||
}
|
|
||||||
catch (Throwable e)
|
|
||||||
{
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_server.start();
|
|
||||||
assertThat(_context.getBaseResource().isAlias(), equalTo(false));
|
|
||||||
|
|
||||||
// We can access web.xml with ServletContext.getResource().
|
|
||||||
InputStream webXml = resource.get(5, TimeUnit.SECONDS);
|
|
||||||
assertNotNull(webXml);
|
|
||||||
String content = IO.toString(webXml);
|
|
||||||
assertThat(content, equalTo("This is the web.xml file."));
|
|
||||||
|
|
||||||
// Can access normal files in the webroot dir.
|
|
||||||
URI uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/file");
|
|
||||||
ContentResponse response = _client.GET(uri);
|
|
||||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
|
||||||
assertThat(response.getContentAsString(), is("This file is inside webroot."));
|
|
||||||
|
|
||||||
// Cannot access web.xml with an external request.
|
|
||||||
uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/WEB-INF/web.xml");
|
|
||||||
response = _client.GET(uri);
|
|
||||||
assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404));
|
|
||||||
assertThat(response.getContentAsString(), not(containsString("This file is inside webroot.")));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under the
|
|
||||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
|
||||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package org.eclipse.jetty.ee9.nested;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.util.resource.PathResource;
|
|
||||||
import org.eclipse.jetty.util.resource.Resource;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Symbolic Link AliasChecker.
|
|
||||||
* <p>An instance of this class can be registered with {@link ContextHandler#addAliasCheck(ContextHandler.AliasCheck)}
|
|
||||||
* to check resources that are aliased to other locations. The checker uses the
|
|
||||||
* Java {@link Files#readSymbolicLink(Path)} and {@link Path#toRealPath(java.nio.file.LinkOption...)}
|
|
||||||
* APIs to check if a file is aliased with symbolic links.</p>
|
|
||||||
* @deprecated use {@link SymlinkAllowedResourceAliasChecker} instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public class AllowSymLinkAliasChecker implements ContextHandler.AliasCheck
|
|
||||||
{
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AllowSymLinkAliasChecker.class);
|
|
||||||
|
|
||||||
public AllowSymLinkAliasChecker()
|
|
||||||
{
|
|
||||||
LOG.warn("Deprecated, use SymlinkAllowedResourceAliasChecker instead.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean check(String pathInContext, Resource resource)
|
|
||||||
{
|
|
||||||
// Only support PathResource alias checking
|
|
||||||
if (!(resource instanceof PathResource))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
PathResource pathResource = (PathResource)resource;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Path path = pathResource.getPath();
|
|
||||||
Path alias = pathResource.getAliasPath();
|
|
||||||
|
|
||||||
if (PathResource.isSameName(alias, path))
|
|
||||||
return false; // Unknown why this is an alias
|
|
||||||
|
|
||||||
if (hasSymbolicLink(path) && Files.isSameFile(path, alias))
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Allow symlink {} --> {}", resource, pathResource.getAliasPath());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LOG.trace("IGNORED", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasSymbolicLink(Path path)
|
|
||||||
{
|
|
||||||
// Is file itself a symlink?
|
|
||||||
if (Files.isSymbolicLink(path))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lets try each path segment
|
|
||||||
Path base = path.getRoot();
|
|
||||||
for (Path segment : path)
|
|
||||||
{
|
|
||||||
base = base.resolve(segment);
|
|
||||||
if (Files.isSymbolicLink(base))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,237 +0,0 @@
|
||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under the
|
|
||||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
|
||||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package org.eclipse.jetty.ee9.nested;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
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.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.util.component.AbstractLifeCycle;
|
|
||||||
import org.eclipse.jetty.util.component.LifeCycle;
|
|
||||||
import org.eclipse.jetty.util.resource.PathResource;
|
|
||||||
import org.eclipse.jetty.util.resource.Resource;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>This will approve any alias to anything inside of the {@link ContextHandler}s resource base which
|
|
||||||
* is not protected by a protected target as defined by getProtectedTargets at start.</p>
|
|
||||||
* <p>Aliases approved by this may still be able to bypass SecurityConstraints, so this class would need to be extended
|
|
||||||
* to enforce any additional security constraints that are required.</p>
|
|
||||||
*/
|
|
||||||
public class AllowedResourceAliasChecker extends AbstractLifeCycle implements ContextHandler.AliasCheck
|
|
||||||
{
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AllowedResourceAliasChecker.class);
|
|
||||||
protected static final LinkOption[] FOLLOW_LINKS = new LinkOption[0];
|
|
||||||
protected static final LinkOption[] NO_FOLLOW_LINKS = new LinkOption[]{LinkOption.NOFOLLOW_LINKS};
|
|
||||||
|
|
||||||
private final ContextHandler _contextHandler;
|
|
||||||
private final List<Path> _protected = new ArrayList<>();
|
|
||||||
private final AllowedResourceAliasCheckListener _listener = new AllowedResourceAliasCheckListener();
|
|
||||||
protected Path _base;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param contextHandler the context handler to use.
|
|
||||||
*/
|
|
||||||
public AllowedResourceAliasChecker(ContextHandler contextHandler)
|
|
||||||
{
|
|
||||||
_contextHandler = Objects.requireNonNull(contextHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ContextHandler getContextHandler()
|
|
||||||
{
|
|
||||||
return _contextHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void initialize()
|
|
||||||
{
|
|
||||||
_base = _contextHandler.getCoreContextHandler().getBaseResource().getPath();
|
|
||||||
if (_base == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Files.exists(_base, NO_FOLLOW_LINKS))
|
|
||||||
_base = _base.toRealPath(FOLLOW_LINKS);
|
|
||||||
String[] protectedTargets = _contextHandler.getProtectedTargets();
|
|
||||||
if (protectedTargets != null)
|
|
||||||
{
|
|
||||||
for (String s : protectedTargets)
|
|
||||||
_protected.add(_base.getFileSystem().getPath(_base.toString(), s));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
LOG.warn("Base resource failure ({} is disabled): {}", this.getClass().getName(), _base, e);
|
|
||||||
_base = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doStart() throws Exception
|
|
||||||
{
|
|
||||||
// We can only initialize if ContextHandler in started state, the baseResource can be changed even in starting state.
|
|
||||||
// If the ContextHandler is not started add a listener to delay initialization until fully started.
|
|
||||||
if (_contextHandler.isStarted())
|
|
||||||
initialize();
|
|
||||||
else
|
|
||||||
_contextHandler.addEventListener(_listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doStop() throws Exception
|
|
||||||
{
|
|
||||||
_contextHandler.removeEventListener(_listener);
|
|
||||||
_base = null;
|
|
||||||
_protected.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean check(String pathInContext, Resource resource)
|
|
||||||
{
|
|
||||||
if (_base == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// The existence check resolves the symlinks.
|
|
||||||
if (!resource.exists())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
Path path = getPath(resource);
|
|
||||||
if (path == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return check(pathInContext, path);
|
|
||||||
}
|
|
||||||
catch (Throwable t)
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Failed to check alias", t);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean check(String pathInContext, Path path)
|
|
||||||
{
|
|
||||||
// Allow any aliases (symlinks, 8.3, casing, etc.) so long as
|
|
||||||
// the resulting real file is allowed.
|
|
||||||
return isAllowed(getRealPath(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isAllowed(Path path)
|
|
||||||
{
|
|
||||||
// If the resource doesn't exist we cannot determine whether it is protected so we assume it is.
|
|
||||||
if (path != null && Files.exists(path))
|
|
||||||
{
|
|
||||||
// Walk the path parent links looking for the base resource, but failing if any steps are protected
|
|
||||||
while (path != null)
|
|
||||||
{
|
|
||||||
// If the path is the same file as the base, then it is contained in the base and
|
|
||||||
// is not protected.
|
|
||||||
if (isSameFile(path, _base))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// If the path is the same file as any protected resources, then it is protected.
|
|
||||||
for (Path p : _protected)
|
|
||||||
{
|
|
||||||
if (isSameFile(path, p))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walks up the aliased path name, not the real path name.
|
|
||||||
// If WEB-INF is a link to /var/lib/webmeta then after checking
|
|
||||||
// a URI of /WEB-INF/file.xml the parent is /WEB-INF and not /var/lib/webmeta
|
|
||||||
path = path.getParent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isSameFile(Path path1, Path path2)
|
|
||||||
{
|
|
||||||
if (Objects.equals(path1, path2))
|
|
||||||
return true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Files.isSameFile(path1, path2))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Throwable t)
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("ignored", t);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Path getRealPath(Path path)
|
|
||||||
{
|
|
||||||
if (path == null || !Files.exists(path))
|
|
||||||
return null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
path = path.toRealPath(FOLLOW_LINKS);
|
|
||||||
if (Files.exists(path))
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("No real path for {}", path, e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Path getPath(Resource resource)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (resource instanceof PathResource)
|
|
||||||
return resource.getPath();
|
|
||||||
return (resource == null) ? null : resource.getPath();
|
|
||||||
}
|
|
||||||
catch (Throwable t)
|
|
||||||
{
|
|
||||||
LOG.trace("getPath() failed", t);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AllowedResourceAliasCheckListener implements LifeCycle.Listener
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void lifeCycleStarted(LifeCycle event)
|
|
||||||
{
|
|
||||||
initialize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
String[] protectedTargets = _contextHandler.getProtectedTargets();
|
|
||||||
return String.format("%s@%x{base=%s,protected=%s}",
|
|
||||||
this.getClass().getSimpleName(),
|
|
||||||
hashCode(),
|
|
||||||
_base,
|
|
||||||
(protectedTargets == null) ? null : Arrays.asList(protectedTargets));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -66,6 +66,8 @@ import org.eclipse.jetty.http.HttpMethod;
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
import org.eclipse.jetty.http.HttpURI;
|
import org.eclipse.jetty.http.HttpURI;
|
||||||
import org.eclipse.jetty.http.MimeTypes;
|
import org.eclipse.jetty.http.MimeTypes;
|
||||||
|
import org.eclipse.jetty.server.AliasCheck;
|
||||||
|
import org.eclipse.jetty.server.AllowedResourceAliasChecker;
|
||||||
import org.eclipse.jetty.server.Context;
|
import org.eclipse.jetty.server.Context;
|
||||||
import org.eclipse.jetty.server.Handler;
|
import org.eclipse.jetty.server.Handler;
|
||||||
import org.eclipse.jetty.server.Response;
|
import org.eclipse.jetty.server.Response;
|
||||||
|
@ -75,7 +77,6 @@ import org.eclipse.jetty.server.handler.ContextRequest;
|
||||||
import org.eclipse.jetty.util.Attributes;
|
import org.eclipse.jetty.util.Attributes;
|
||||||
import org.eclipse.jetty.util.Callback;
|
import org.eclipse.jetty.util.Callback;
|
||||||
import org.eclipse.jetty.util.ExceptionUtil;
|
import org.eclipse.jetty.util.ExceptionUtil;
|
||||||
import org.eclipse.jetty.util.Index;
|
|
||||||
import org.eclipse.jetty.util.Loader;
|
import org.eclipse.jetty.util.Loader;
|
||||||
import org.eclipse.jetty.util.MultiMap;
|
import org.eclipse.jetty.util.MultiMap;
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
import org.eclipse.jetty.util.StringUtil;
|
||||||
|
@ -86,7 +87,6 @@ import org.eclipse.jetty.util.annotation.ManagedObject;
|
||||||
import org.eclipse.jetty.util.component.DumpableCollection;
|
import org.eclipse.jetty.util.component.DumpableCollection;
|
||||||
import org.eclipse.jetty.util.component.Environment;
|
import org.eclipse.jetty.util.component.Environment;
|
||||||
import org.eclipse.jetty.util.component.Graceful;
|
import org.eclipse.jetty.util.component.Graceful;
|
||||||
import org.eclipse.jetty.util.component.LifeCycle;
|
|
||||||
import org.eclipse.jetty.util.resource.Resource;
|
import org.eclipse.jetty.util.resource.Resource;
|
||||||
import org.eclipse.jetty.util.resource.ResourceFactory;
|
import org.eclipse.jetty.util.resource.ResourceFactory;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -186,16 +186,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
|
||||||
DESTROYED
|
DESTROYED
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of protected target match
|
|
||||||
* @see #_protectedTargets
|
|
||||||
*/
|
|
||||||
private enum ProtectedTargetType
|
|
||||||
{
|
|
||||||
EXACT,
|
|
||||||
PREFIX
|
|
||||||
}
|
|
||||||
|
|
||||||
private final CoreContextHandler _coreContextHandler;
|
private final CoreContextHandler _coreContextHandler;
|
||||||
protected ContextStatus _contextStatus = ContextStatus.NOTSET;
|
protected ContextStatus _contextStatus = ContextStatus.NOTSET;
|
||||||
protected APIContext _apiContext;
|
protected APIContext _apiContext;
|
||||||
|
@ -221,8 +211,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
|
||||||
private final List<ServletRequestAttributeListener> _servletRequestAttributeListeners = new CopyOnWriteArrayList<>();
|
private final List<ServletRequestAttributeListener> _servletRequestAttributeListeners = new CopyOnWriteArrayList<>();
|
||||||
private final List<ContextScopeListener> _contextListeners = new CopyOnWriteArrayList<>();
|
private final List<ContextScopeListener> _contextListeners = new CopyOnWriteArrayList<>();
|
||||||
private final Set<EventListener> _durableListeners = new HashSet<>();
|
private final Set<EventListener> _durableListeners = new HashSet<>();
|
||||||
private Index<ProtectedTargetType> _protectedTargets = Index.empty(false);
|
|
||||||
private final List<AliasCheck> _aliasChecks = new CopyOnWriteArrayList<>();
|
|
||||||
|
|
||||||
public ContextHandler()
|
public ContextHandler()
|
||||||
{
|
{
|
||||||
|
@ -882,7 +870,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
|
||||||
if (new_context)
|
if (new_context)
|
||||||
requestInitialized(baseRequest, request);
|
requestInitialized(baseRequest, request);
|
||||||
|
|
||||||
if (new_context && isProtectedTarget(target))
|
if (new_context && _coreContextHandler.isProtectedTarget(target))
|
||||||
{
|
{
|
||||||
baseRequest.setHandled(true);
|
baseRequest.setHandled(true);
|
||||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
@ -989,16 +977,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
|
||||||
*/
|
*/
|
||||||
public boolean isProtectedTarget(String target)
|
public boolean isProtectedTarget(String target)
|
||||||
{
|
{
|
||||||
if (target == null || _protectedTargets.isEmpty())
|
return _coreContextHandler.isProtectedTarget(target);
|
||||||
return false;
|
|
||||||
|
|
||||||
if (target.startsWith("//"))
|
|
||||||
target = URIUtil.compactPath(target);
|
|
||||||
|
|
||||||
ProtectedTargetType type = _protectedTargets.getBest(target);
|
|
||||||
|
|
||||||
return type == ProtectedTargetType.PREFIX ||
|
|
||||||
type == ProtectedTargetType.EXACT && _protectedTargets.get(target) == ProtectedTargetType.EXACT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1006,32 +985,12 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
|
||||||
*/
|
*/
|
||||||
public void setProtectedTargets(String[] targets)
|
public void setProtectedTargets(String[] targets)
|
||||||
{
|
{
|
||||||
Index.Builder<ProtectedTargetType> builder = new Index.Builder<>();
|
_coreContextHandler.setProtectedTargets(targets);
|
||||||
if (targets != null)
|
|
||||||
{
|
|
||||||
for (String t : targets)
|
|
||||||
{
|
|
||||||
if (!t.startsWith("/"))
|
|
||||||
throw new IllegalArgumentException("Bad protected target: " + t);
|
|
||||||
|
|
||||||
builder.with(t, ProtectedTargetType.EXACT);
|
|
||||||
builder.with(t + "/", ProtectedTargetType.PREFIX);
|
|
||||||
builder.with(t + "?", ProtectedTargetType.PREFIX);
|
|
||||||
builder.with(t + "#", ProtectedTargetType.PREFIX);
|
|
||||||
builder.with(t + ";", ProtectedTargetType.PREFIX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_protectedTargets = builder.caseSensitive(false).build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String[] getProtectedTargets()
|
public String[] getProtectedTargets()
|
||||||
{
|
{
|
||||||
if (_protectedTargets == null)
|
return _coreContextHandler.getProtectedTargets();
|
||||||
return null;
|
|
||||||
|
|
||||||
return _protectedTargets.keySet().stream()
|
|
||||||
.filter(s -> _protectedTargets.get(s) == ProtectedTargetType.EXACT)
|
|
||||||
.toArray(String[]::new);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1448,9 +1407,9 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
|
||||||
LOG.debug("Aliased resource: {}~={}", resource, resource.getAlias());
|
LOG.debug("Aliased resource: {}~={}", resource, resource.getAlias());
|
||||||
|
|
||||||
// alias checks
|
// alias checks
|
||||||
for (AliasCheck check : _aliasChecks)
|
for (AliasCheck check : getAliasChecks())
|
||||||
{
|
{
|
||||||
if (check.check(path, resource))
|
if (check.checkAlias(path, resource))
|
||||||
{
|
{
|
||||||
if (LOG.isDebugEnabled())
|
if (LOG.isDebugEnabled())
|
||||||
LOG.debug("Aliased resource: {} approved by {}", resource, check);
|
LOG.debug("Aliased resource: {} approved by {}", resource, check);
|
||||||
|
@ -1555,11 +1514,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
|
||||||
*/
|
*/
|
||||||
public void addAliasCheck(AliasCheck check)
|
public void addAliasCheck(AliasCheck check)
|
||||||
{
|
{
|
||||||
_aliasChecks.add(check);
|
_coreContextHandler.addAliasCheck(check);
|
||||||
if (check instanceof LifeCycle)
|
|
||||||
addManaged((LifeCycle)check);
|
|
||||||
else
|
|
||||||
addBean(check);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1567,7 +1522,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
|
||||||
*/
|
*/
|
||||||
public List<AliasCheck> getAliasChecks()
|
public List<AliasCheck> getAliasChecks()
|
||||||
{
|
{
|
||||||
return Collections.unmodifiableList(_aliasChecks);
|
return _coreContextHandler.getAliasChecks();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1575,8 +1530,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
|
||||||
*/
|
*/
|
||||||
public void setAliasChecks(List<AliasCheck> checks)
|
public void setAliasChecks(List<AliasCheck> checks)
|
||||||
{
|
{
|
||||||
clearAliasChecks();
|
_coreContextHandler.setAliasChecks(checks);
|
||||||
checks.forEach(this::addAliasCheck);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1584,8 +1538,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
|
||||||
*/
|
*/
|
||||||
public void clearAliasChecks()
|
public void clearAliasChecks()
|
||||||
{
|
{
|
||||||
_aliasChecks.forEach(this::removeBean);
|
_coreContextHandler.clearAliasChecks();
|
||||||
_aliasChecks.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle a request from a connection.
|
/* Handle a request from a connection.
|
||||||
|
@ -2363,65 +2316,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface to check aliases
|
|
||||||
*/
|
|
||||||
public interface AliasCheck
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check an alias
|
|
||||||
*
|
|
||||||
* @param pathInContext The path the aliased resource was created for
|
|
||||||
* @param resource The aliased resourced
|
|
||||||
* @return True if the resource is OK to be served.
|
|
||||||
*/
|
|
||||||
boolean check(String pathInContext, Resource resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Approve all aliases.
|
|
||||||
* @deprecated use {@link org.eclipse.jetty.server.AllowedResourceAliasChecker} instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static class ApproveAliases implements AliasCheck
|
|
||||||
{
|
|
||||||
public ApproveAliases()
|
|
||||||
{
|
|
||||||
LOG.warn("ApproveAliases is deprecated");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean check(String pathInContext, Resource resource)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Approve Aliases of a non existent directory. If a directory "/foobar/" does not exist, then the resource is aliased to "/foobar". Accept such aliases.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public static class ApproveNonExistentDirectoryAliases implements AliasCheck
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public boolean check(String pathInContext, Resource resource)
|
|
||||||
{
|
|
||||||
if (resource.exists())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
String a = resource.getAlias().toString();
|
|
||||||
String r = resource.getURI().toString();
|
|
||||||
|
|
||||||
if (a.length() > r.length())
|
|
||||||
return a.startsWith(r) && a.length() == r.length() + 1 && a.endsWith("/");
|
|
||||||
if (a.length() < r.length())
|
|
||||||
return r.startsWith(a) && r.length() == a.length() + 1 && r.endsWith("/");
|
|
||||||
|
|
||||||
return a.equals(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener for all threads entering context scope, including async IO callbacks
|
* Listener for all threads entering context scope, including async IO callbacks
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under the
|
|
||||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
|
||||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package org.eclipse.jetty.ee9.nested;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.util.resource.PathResource;
|
|
||||||
import org.eclipse.jetty.util.resource.Resource;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alias checking for working with FileSystems that normalize access to the
|
|
||||||
* File System.
|
|
||||||
* <p>
|
|
||||||
* The Java {@link Files#isSameFile(Path, Path)} method is used to determine
|
|
||||||
* if the requested file is the same as the alias file.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* For File Systems that are case insensitive (eg: Microsoft Windows FAT32 and NTFS),
|
|
||||||
* the access to the file can be in any combination or style of upper and lowercase.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* For File Systems that normalize UTF-8 access (eg: Mac OSX on HFS+ or APFS,
|
|
||||||
* or Linux on XFS) the the actual file could be stored using UTF-16,
|
|
||||||
* but be accessed using NFD UTF-8 or NFC UTF-8 for the same file.
|
|
||||||
* </p>
|
|
||||||
* @deprecated use {@link AllowedResourceAliasChecker} instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public class SameFileAliasChecker implements ContextHandler.AliasCheck
|
|
||||||
{
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(SameFileAliasChecker.class);
|
|
||||||
|
|
||||||
public SameFileAliasChecker()
|
|
||||||
{
|
|
||||||
LOG.warn("SameFileAliasChecker is deprecated");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean check(String pathInContext, Resource resource)
|
|
||||||
{
|
|
||||||
// Do not allow any file separation characters in the URI.
|
|
||||||
if (File.separatorChar != '/' && pathInContext.indexOf(File.separatorChar) >= 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Only support PathResource alias checking
|
|
||||||
if (!(resource instanceof PathResource))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
PathResource pathResource = (PathResource)resource;
|
|
||||||
Path path = pathResource.getPath();
|
|
||||||
Path alias = pathResource.getAliasPath();
|
|
||||||
|
|
||||||
if (Files.isSameFile(path, alias))
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Allow alias to same file {} --> {}", path, alias);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
LOG.trace("IGNORED", e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under the
|
|
||||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
|
||||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package org.eclipse.jetty.ee9.nested;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An extension of {@link AllowedResourceAliasChecker} which will allow symlinks alias to arbitrary
|
|
||||||
* targets, so long as the symlink file itself is an allowed resource.
|
|
||||||
*/
|
|
||||||
public class SymlinkAllowedResourceAliasChecker extends AllowedResourceAliasChecker
|
|
||||||
{
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(SymlinkAllowedResourceAliasChecker.class);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param contextHandler the context handler to use.
|
|
||||||
*/
|
|
||||||
public SymlinkAllowedResourceAliasChecker(ContextHandler contextHandler)
|
|
||||||
{
|
|
||||||
super(contextHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean check(String pathInContext, Path path)
|
|
||||||
{
|
|
||||||
if (_base == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// do not allow any file separation characters in the URI, as we need to know exactly what are the segments
|
|
||||||
if (File.separatorChar != '/' && pathInContext.indexOf(File.separatorChar) >= 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Split the URI path into segments, to walk down the resource tree and build the realURI of any symlink found
|
|
||||||
// We rebuild the realURI, segment by segment, getting the real name at each step, so that we can distinguish between
|
|
||||||
// alias types. Specifically, so we can allow a symbolic link so long as it's realpath is not protected.
|
|
||||||
String[] segments = pathInContext.substring(1).split("/");
|
|
||||||
Path fromBase = _base;
|
|
||||||
StringBuilder realURI = new StringBuilder();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
for (String segment : segments)
|
|
||||||
{
|
|
||||||
// Add the segment to the path and realURI.
|
|
||||||
fromBase = fromBase.resolve(segment);
|
|
||||||
realURI.append("/").append(fromBase.toRealPath(NO_FOLLOW_LINKS).getFileName());
|
|
||||||
|
|
||||||
// If the ancestor of the alias is a symlink, then check if the real URI is protected, otherwise allow.
|
|
||||||
// This allows symlinks like /other->/WEB-INF and /external->/var/lib/docroot
|
|
||||||
// This does not allow symlinks like /WeB-InF->/var/lib/other
|
|
||||||
if (Files.isSymbolicLink(fromBase))
|
|
||||||
return !getContextHandler().isProtectedTarget(realURI.toString());
|
|
||||||
|
|
||||||
// If the ancestor is not allowed then do not allow.
|
|
||||||
if (!isAllowed(fromBase))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// TODO as we are building the realURI of the resource, it would be possible to
|
|
||||||
// re-check that against security constraints.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Throwable t)
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Failed to check alias", t);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No symlink found, so must be allowed. Double check it is the right path we checked.
|
|
||||||
return isSameFile(fromBase, path);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -40,10 +40,8 @@ import jakarta.servlet.ServletResponse;
|
||||||
import jakarta.servlet.http.HttpServlet;
|
import jakarta.servlet.http.HttpServlet;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.eclipse.jetty.ee9.nested.AllowedResourceAliasChecker;
|
|
||||||
import org.eclipse.jetty.ee9.nested.ResourceContentFactory;
|
import org.eclipse.jetty.ee9.nested.ResourceContentFactory;
|
||||||
import org.eclipse.jetty.ee9.nested.ResourceService;
|
import org.eclipse.jetty.ee9.nested.ResourceService;
|
||||||
import org.eclipse.jetty.ee9.nested.SymlinkAllowedResourceAliasChecker;
|
|
||||||
import org.eclipse.jetty.http.DateGenerator;
|
import org.eclipse.jetty.http.DateGenerator;
|
||||||
import org.eclipse.jetty.http.HttpContent;
|
import org.eclipse.jetty.http.HttpContent;
|
||||||
import org.eclipse.jetty.http.HttpField;
|
import org.eclipse.jetty.http.HttpField;
|
||||||
|
@ -51,9 +49,11 @@ import org.eclipse.jetty.http.HttpHeader;
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
import org.eclipse.jetty.http.HttpTester;
|
import org.eclipse.jetty.http.HttpTester;
|
||||||
import org.eclipse.jetty.logging.StacklessLogging;
|
import org.eclipse.jetty.logging.StacklessLogging;
|
||||||
|
import org.eclipse.jetty.server.AllowedResourceAliasChecker;
|
||||||
import org.eclipse.jetty.server.HttpConfiguration;
|
import org.eclipse.jetty.server.HttpConfiguration;
|
||||||
import org.eclipse.jetty.server.LocalConnector;
|
import org.eclipse.jetty.server.LocalConnector;
|
||||||
import org.eclipse.jetty.server.Server;
|
import org.eclipse.jetty.server.Server;
|
||||||
|
import org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker;
|
||||||
import org.eclipse.jetty.toolchain.test.FS;
|
import org.eclipse.jetty.toolchain.test.FS;
|
||||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||||
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
|
||||||
|
@ -1132,7 +1132,7 @@ public class DefaultServletTest
|
||||||
response = HttpTester.parseResponse(rawResponse);
|
response = HttpTester.parseResponse(rawResponse);
|
||||||
assertThat(response.toString(), response.getStatus(), is(HttpStatus.NOT_FOUND_404));
|
assertThat(response.toString(), response.getStatus(), is(HttpStatus.NOT_FOUND_404));
|
||||||
|
|
||||||
context.addAliasCheck(new SymlinkAllowedResourceAliasChecker(context));
|
context.addAliasCheck(new SymlinkAllowedResourceAliasChecker(context.getCoreContextHandler()));
|
||||||
rawResponse = connector.getResponse("GET /context/dir/link.txt HTTP/1.0\r\n\r\n");
|
rawResponse = connector.getResponse("GET /context/dir/link.txt HTTP/1.0\r\n\r\n");
|
||||||
response = HttpTester.parseResponse(rawResponse);
|
response = HttpTester.parseResponse(rawResponse);
|
||||||
assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
|
assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
|
||||||
|
@ -2108,7 +2108,7 @@ public class DefaultServletTest
|
||||||
FS.ensureEmpty(docRoot);
|
FS.ensureEmpty(docRoot);
|
||||||
|
|
||||||
context.addServlet(DefaultServlet.class, "/");
|
context.addServlet(DefaultServlet.class, "/");
|
||||||
context.addAliasCheck(new AllowedResourceAliasChecker(context));
|
context.addAliasCheck(new AllowedResourceAliasChecker(context.getCoreContextHandler()));
|
||||||
|
|
||||||
// Create file with UTF-8 NFC format
|
// Create file with UTF-8 NFC format
|
||||||
String filename = "swedish-" + new String(TypeUtil.fromHexString("C3A5"), UTF_8) + ".txt";
|
String filename = "swedish-" + new String(TypeUtil.fromHexString("C3A5"), UTF_8) + ".txt";
|
||||||
|
@ -2148,7 +2148,7 @@ public class DefaultServletTest
|
||||||
FS.ensureEmpty(docRoot);
|
FS.ensureEmpty(docRoot);
|
||||||
|
|
||||||
context.addServlet(DefaultServlet.class, "/");
|
context.addServlet(DefaultServlet.class, "/");
|
||||||
context.addAliasCheck(new AllowedResourceAliasChecker(context));
|
context.addAliasCheck(new AllowedResourceAliasChecker(context.getCoreContextHandler()));
|
||||||
|
|
||||||
// Create file with UTF-8 NFD format
|
// Create file with UTF-8 NFD format
|
||||||
String filename = "swedish-a" + new String(TypeUtil.fromHexString("CC8A"), UTF_8) + ".txt";
|
String filename = "swedish-a" + new String(TypeUtil.fromHexString("CC8A"), UTF_8) + ".txt";
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
<groupId>org.eclipse.jetty</groupId>
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
<artifactId>jetty-project</artifactId>
|
<artifactId>jetty-project</artifactId>
|
||||||
<version>12.0.0-SNAPSHOT</version>
|
<version>12.0.0-SNAPSHOT</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>org.eclipse.jetty.tests</groupId>
|
<groupId>org.eclipse.jetty.tests</groupId>
|
||||||
<artifactId>tests</artifactId>
|
<artifactId>tests</artifactId>
|
||||||
|
@ -23,6 +22,7 @@
|
||||||
<!-- TODO -->
|
<!-- TODO -->
|
||||||
<!-- module>jetty-jmh</module -->
|
<!-- module>jetty-jmh</module -->
|
||||||
<module>test-distribution</module>
|
<module>test-distribution</module>
|
||||||
|
<module>test-integration</module>
|
||||||
<!-- module>test-jpms</module -->
|
<!-- module>test-jpms</module -->
|
||||||
</modules>
|
</modules>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<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">
|
||||||
|
<parent>
|
||||||
|
<groupId>org.eclipse.jetty.tests</groupId>
|
||||||
|
<artifactId>tests</artifactId>
|
||||||
|
<version>12.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<artifactId>test-integration</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<name>Jetty Tests :: Integrations</name>
|
||||||
|
<properties>
|
||||||
|
<bundle-symbolic-name>${project.groupId}.integrations</bundle-symbolic-name>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-server</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-slf4j-impl</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty.toolchain</groupId>
|
||||||
|
<artifactId>jetty-test-helper</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
|
@ -22,16 +22,16 @@ import java.nio.file.Path;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
import org.eclipse.jetty.client.api.ContentResponse;
|
import org.eclipse.jetty.client.api.ContentResponse;
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
|
import org.eclipse.jetty.server.AliasCheck;
|
||||||
|
import org.eclipse.jetty.server.Handler;
|
||||||
import org.eclipse.jetty.server.Server;
|
import org.eclipse.jetty.server.Server;
|
||||||
import org.eclipse.jetty.server.ServerConnector;
|
import org.eclipse.jetty.server.ServerConnector;
|
||||||
import org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker;
|
import org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker;
|
||||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||||
import org.eclipse.jetty.servlet.DefaultServlet;
|
import org.eclipse.jetty.server.handler.ResourceHandler;
|
||||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
|
||||||
import org.eclipse.jetty.servlet.ServletHolder;
|
|
||||||
import org.eclipse.jetty.util.IO;
|
import org.eclipse.jetty.util.IO;
|
||||||
import org.eclipse.jetty.util.resource.PathResource;
|
|
||||||
import org.eclipse.jetty.util.resource.Resource;
|
import org.eclipse.jetty.util.resource.Resource;
|
||||||
|
import org.eclipse.jetty.util.resource.ResourceFactory;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -45,7 +45,7 @@ public class AliasCheckerMultipleResourceBasesTest
|
||||||
private Server _server;
|
private Server _server;
|
||||||
private ServerConnector _connector;
|
private ServerConnector _connector;
|
||||||
private HttpClient _client;
|
private HttpClient _client;
|
||||||
private ServletContextHandler _context;
|
private ContextHandler _context;
|
||||||
private Path _webRootPath;
|
private Path _webRootPath;
|
||||||
private Path _altDir1Symlink;
|
private Path _altDir1Symlink;
|
||||||
private Path _altDir2Symlink;
|
private Path _altDir2Symlink;
|
||||||
|
@ -62,12 +62,12 @@ public class AliasCheckerMultipleResourceBasesTest
|
||||||
IO.delete(path.toFile());
|
IO.delete(path.toFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setAliasCheckers(ContextHandler.AliasCheck... aliasChecks)
|
private void setAliasCheckers(AliasCheck... aliasChecks)
|
||||||
{
|
{
|
||||||
_context.clearAliasChecks();
|
_context.clearAliasChecks();
|
||||||
if (aliasChecks != null)
|
if (aliasChecks != null)
|
||||||
{
|
{
|
||||||
for (ContextHandler.AliasCheck aliasCheck : aliasChecks)
|
for (AliasCheck aliasCheck : aliasChecks)
|
||||||
{
|
{
|
||||||
_context.addAliasCheck(aliasCheck);
|
_context.addAliasCheck(aliasCheck);
|
||||||
}
|
}
|
||||||
|
@ -93,12 +93,10 @@ public class AliasCheckerMultipleResourceBasesTest
|
||||||
_server = new Server();
|
_server = new Server();
|
||||||
_connector = new ServerConnector(_server);
|
_connector = new ServerConnector(_server);
|
||||||
_server.addConnector(_connector);
|
_server.addConnector(_connector);
|
||||||
_context = new ServletContextHandler();
|
_context = new ContextHandler();
|
||||||
|
|
||||||
_context.setContextPath("/");
|
_context.setContextPath("/");
|
||||||
_context.setBaseResource(new PathResource(_webRootPath));
|
_context.setBaseResource(_webRootPath);
|
||||||
_context.setWelcomeFiles(new String[]{"index.html"});
|
|
||||||
_context.getMimeTypes().addMimeMapping("txt", "text/plain;charset=utf-8");
|
|
||||||
_server.setHandler(_context);
|
_server.setHandler(_context);
|
||||||
_context.clearAliasChecks();
|
_context.clearAliasChecks();
|
||||||
|
|
||||||
|
@ -116,44 +114,54 @@ public class AliasCheckerMultipleResourceBasesTest
|
||||||
_server.stop();
|
_server.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ResourceHandler newResourceHandler(Path resourceBase)
|
||||||
|
{
|
||||||
|
Resource resource = toResource(resourceBase);
|
||||||
|
ResourceHandler resourceHandler = new ResourceHandler();
|
||||||
|
resourceHandler.setBaseResource(resource);
|
||||||
|
return resourceHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Resource toResource(Path path)
|
||||||
|
{
|
||||||
|
return ResourceFactory.root().newResource(path);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test() throws Exception
|
public void test() throws Exception
|
||||||
{
|
{
|
||||||
ServletHolder servletHolder;
|
Handler.Collection collection = new Handler.Collection();
|
||||||
servletHolder = _context.addServlet(DefaultServlet.class, "/defaultServlet1/*");
|
collection.addHandler(newResourceHandler(_altDir1Symlink));
|
||||||
servletHolder.setInitParameter("resourceBase", _altDir1Symlink.toString());
|
collection.addHandler(newResourceHandler(_altDir2Symlink));
|
||||||
servletHolder.setInitParameter("pathInfoOnly", "true");
|
_context.setHandler(collection);
|
||||||
servletHolder = _context.addServlet(DefaultServlet.class, "/defaultServlet2/*");
|
|
||||||
servletHolder.setInitParameter("resourceBase", _altDir2Symlink.toString());
|
|
||||||
servletHolder.setInitParameter("pathInfoOnly", "true");
|
|
||||||
|
|
||||||
setAliasCheckers(
|
|
||||||
new SymlinkAllowedResourceAliasChecker(_context, Resource.newResource(_altDir1Symlink)),
|
|
||||||
new SymlinkAllowedResourceAliasChecker(_context, Resource.newResource(_altDir2Symlink))
|
|
||||||
);
|
|
||||||
|
|
||||||
_server.start();
|
_server.start();
|
||||||
|
|
||||||
// Can access file 1 only through default servlet 1.
|
// With no alias checkers we cannot access file 1.
|
||||||
URI uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/defaultServlet1/file1");
|
URI uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/file1");
|
||||||
ContentResponse response = _client.GET(uri);
|
ContentResponse response = _client.GET(uri);
|
||||||
|
assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404));
|
||||||
|
|
||||||
|
// With no alias checkers we cannot access file 2.
|
||||||
|
uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/file2");
|
||||||
|
response = _client.GET(uri);
|
||||||
|
assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404));
|
||||||
|
|
||||||
|
// Set alias checkers to allow content under these alternative resource bases.
|
||||||
|
setAliasCheckers(
|
||||||
|
new SymlinkAllowedResourceAliasChecker(_context, toResource(_altDir1Symlink)),
|
||||||
|
new SymlinkAllowedResourceAliasChecker(_context, toResource(_altDir2Symlink))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Now we have set alias checkers we can access file 1.
|
||||||
|
uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/file1");
|
||||||
|
response = _client.GET(uri);
|
||||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||||
assertThat(response.getContentAsString(), is("file 1 contents"));
|
assertThat(response.getContentAsString(), is("file 1 contents"));
|
||||||
|
|
||||||
// File 2 cannot be found with default servlet 1.
|
// Now we have set alias checkers we can access file 2.
|
||||||
uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/defaultServlet1/file2");
|
uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/file2");
|
||||||
response = _client.GET(uri);
|
|
||||||
assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404));
|
|
||||||
|
|
||||||
// Can access file 2 only through default servlet 2.
|
|
||||||
uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/defaultServlet2/file2");
|
|
||||||
response = _client.GET(uri);
|
response = _client.GET(uri);
|
||||||
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
assertThat(response.getStatus(), is(HttpStatus.OK_200));
|
||||||
assertThat(response.getContentAsString(), is("file 2 contents"));
|
assertThat(response.getContentAsString(), is("file 2 contents"));
|
||||||
|
|
||||||
// File 1 cannot be found with default servlet 2.
|
|
||||||
uri = URI.create("http://localhost:" + _connector.getLocalPort() + "/defaultServlet2/file1");
|
|
||||||
response = _client.GET(uri);
|
|
||||||
assertThat(response.getStatus(), is(HttpStatus.NOT_FOUND_404));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,219 @@
|
||||||
|
//
|
||||||
|
// ========================================================================
|
||||||
|
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||||
|
//
|
||||||
|
// This program and the accompanying materials are made available under the
|
||||||
|
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||||
|
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||||
|
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||||
|
// ========================================================================
|
||||||
|
//
|
||||||
|
|
||||||
|
package org.eclipse.jetty.test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
|
import org.eclipse.jetty.client.api.ContentResponse;
|
||||||
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
|
import org.eclipse.jetty.server.AliasCheck;
|
||||||
|
import org.eclipse.jetty.server.AllowedResourceAliasChecker;
|
||||||
|
import org.eclipse.jetty.server.Server;
|
||||||
|
import org.eclipse.jetty.server.ServerConnector;
|
||||||
|
import org.eclipse.jetty.server.SymlinkAllowedResourceAliasChecker;
|
||||||
|
import org.eclipse.jetty.server.handler.ContextHandler;
|
||||||
|
import org.eclipse.jetty.server.handler.ResourceHandler;
|
||||||
|
import org.eclipse.jetty.util.IO;
|
||||||
|
import org.eclipse.jetty.util.resource.Resource;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
|
public class AliasCheckerSymlinkTest
|
||||||
|
{
|
||||||
|
private static Server _server;
|
||||||
|
private static ServerConnector _connector;
|
||||||
|
private static HttpClient _client;
|
||||||
|
private static ContextHandler _context;
|
||||||
|
|
||||||
|
private static Path _symlinkFile;
|
||||||
|
private static Path _symlinkExternalFile;
|
||||||
|
private static Path _symlinkDir;
|
||||||
|
private static Path _symlinkParentDir;
|
||||||
|
private static Path _symlinkSiblingDir;
|
||||||
|
private static Path _webInfSymlink;
|
||||||
|
private static Path _webrootSymlink;
|
||||||
|
|
||||||
|
private static Path getResource(String path) throws Exception
|
||||||
|
{
|
||||||
|
URL url = AliasCheckerSymlinkTest.class.getClassLoader().getResource(path);
|
||||||
|
assertNotNull(url);
|
||||||
|
return new File(url.toURI()).toPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void delete(Path path)
|
||||||
|
{
|
||||||
|
IO.delete(path.toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setAliasChecker(AliasCheck aliasChecker)
|
||||||
|
{
|
||||||
|
_context.clearAliasChecks();
|
||||||
|
if (aliasChecker != null)
|
||||||
|
_context.addAliasCheck(aliasChecker);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void beforeAll() throws Exception
|
||||||
|
{
|
||||||
|
Path webRootPath = getResource("webroot");
|
||||||
|
Path fileInWebroot = webRootPath.resolve("file");
|
||||||
|
|
||||||
|
// Create symlink file that targets inside the webroot directory.
|
||||||
|
_symlinkFile = webRootPath.resolve("symlinkFile");
|
||||||
|
delete(_symlinkFile);
|
||||||
|
Files.createSymbolicLink(_symlinkFile, fileInWebroot).toFile().deleteOnExit();
|
||||||
|
|
||||||
|
// Create symlink file that targets outside the webroot directory.
|
||||||
|
_symlinkExternalFile = webRootPath.resolve("symlinkExternalFile");
|
||||||
|
delete(_symlinkExternalFile);
|
||||||
|
Files.createSymbolicLink(_symlinkExternalFile, getResource("file")).toFile().deleteOnExit();
|
||||||
|
|
||||||
|
// Symlink to a directory inside of the webroot.
|
||||||
|
_symlinkDir = webRootPath.resolve("symlinkDir");
|
||||||
|
delete(_symlinkDir);
|
||||||
|
Files.createSymbolicLink(_symlinkDir, webRootPath.resolve("documents")).toFile().deleteOnExit();
|
||||||
|
|
||||||
|
// Symlink to a directory parent of the webroot.
|
||||||
|
_symlinkParentDir = webRootPath.resolve("symlinkParentDir");
|
||||||
|
delete(_symlinkParentDir);
|
||||||
|
Files.createSymbolicLink(_symlinkParentDir, webRootPath.resolve("..")).toFile().deleteOnExit();
|
||||||
|
|
||||||
|
// Symlink to a directory outside of the webroot.
|
||||||
|
_symlinkSiblingDir = webRootPath.resolve("symlinkSiblingDir");
|
||||||
|
delete(_symlinkSiblingDir);
|
||||||
|
Files.createSymbolicLink(_symlinkSiblingDir, webRootPath.resolve("../sibling")).toFile().deleteOnExit();
|
||||||
|
|
||||||
|
// Symlink to the WEB-INF directory.
|
||||||
|
_webInfSymlink = webRootPath.resolve("webInfSymlink");
|
||||||
|
delete(_webInfSymlink);
|
||||||
|
Files.createSymbolicLink(_webInfSymlink, webRootPath.resolve("WEB-INF")).toFile().deleteOnExit();
|
||||||
|
|
||||||
|
// External symlink to webroot.
|
||||||
|
_webrootSymlink = webRootPath.resolve("../webrootSymlink");
|
||||||
|
delete(_webrootSymlink);
|
||||||
|
Files.createSymbolicLink(_webrootSymlink, webRootPath).toFile().deleteOnExit();
|
||||||
|
|
||||||
|
// Create and start Server and Client.
|
||||||
|
_server = new Server();
|
||||||
|
_connector = new ServerConnector(_server);
|
||||||
|
_server.addConnector(_connector);
|
||||||
|
_context = new ContextHandler();
|
||||||
|
_context.setContextPath("/");
|
||||||
|
_context.setBaseResource(webRootPath);
|
||||||
|
_context.setProtectedTargets(new String[]{"/WEB-INF", "/META-INF"});
|
||||||
|
_context.addHandler(new ResourceHandler());
|
||||||
|
|
||||||
|
_server.setHandler(_context);
|
||||||
|
_context.clearAliasChecks();
|
||||||
|
_server.start();
|
||||||
|
|
||||||
|
_client = new HttpClient();
|
||||||
|
_client.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
public static void afterAll() throws Exception
|
||||||
|
{
|
||||||
|
// Try to delete all files now so that the symlinks do not confuse other tests.
|
||||||
|
Files.delete(_symlinkFile);
|
||||||
|
Files.delete(_symlinkExternalFile);
|
||||||
|
Files.delete(_symlinkDir);
|
||||||
|
Files.delete(_symlinkParentDir);
|
||||||
|
Files.delete(_symlinkSiblingDir);
|
||||||
|
Files.delete(_webInfSymlink);
|
||||||
|
Files.delete(_webrootSymlink);
|
||||||
|
|
||||||
|
_client.stop();
|
||||||
|
_server.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ApproveAliases implements AliasCheck
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public boolean checkAlias(String pathInContext, Resource resource)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Stream<Arguments> testCases()
|
||||||
|
{
|
||||||
|
AllowedResourceAliasChecker allowedResource = new AllowedResourceAliasChecker(_context);
|
||||||
|
SymlinkAllowedResourceAliasChecker symlinkAllowedResource = new SymlinkAllowedResourceAliasChecker(_context);
|
||||||
|
ApproveAliases approveAliases = new ApproveAliases();
|
||||||
|
|
||||||
|
return Stream.of(
|
||||||
|
// AllowedResourceAliasChecker that checks the target of symlinks.
|
||||||
|
Arguments.of(allowedResource, "/symlinkFile", HttpStatus.OK_200, "This file is inside webroot."),
|
||||||
|
Arguments.of(allowedResource, "/symlinkExternalFile", HttpStatus.NOT_FOUND_404, null),
|
||||||
|
Arguments.of(allowedResource, "/symlinkDir/file", HttpStatus.OK_200, "This file is inside webroot/documents."),
|
||||||
|
Arguments.of(allowedResource, "/symlinkParentDir/webroot/file", HttpStatus.OK_200, "This file is inside webroot."),
|
||||||
|
Arguments.of(allowedResource, "/symlinkParentDir/webroot/WEB-INF/web.xml", HttpStatus.NOT_FOUND_404, null),
|
||||||
|
Arguments.of(allowedResource, "/symlinkSiblingDir/file", HttpStatus.NOT_FOUND_404, null),
|
||||||
|
Arguments.of(allowedResource, "/webInfSymlink/web.xml", HttpStatus.NOT_FOUND_404, null),
|
||||||
|
|
||||||
|
// SymlinkAllowedResourceAliasChecker that does not check the target of symlinks, but only approves files obtained through a symlink.
|
||||||
|
Arguments.of(symlinkAllowedResource, "/symlinkFile", HttpStatus.OK_200, "This file is inside webroot."),
|
||||||
|
Arguments.of(symlinkAllowedResource, "/symlinkExternalFile", HttpStatus.OK_200, "This file is outside webroot."),
|
||||||
|
Arguments.of(symlinkAllowedResource, "/symlinkDir/file", HttpStatus.OK_200, "This file is inside webroot/documents."),
|
||||||
|
Arguments.of(symlinkAllowedResource, "/symlinkParentDir/webroot/file", HttpStatus.OK_200, "This file is inside webroot."),
|
||||||
|
Arguments.of(symlinkAllowedResource, "/symlinkParentDir/webroot/WEB-INF/web.xml", HttpStatus.OK_200, "This is the web.xml file."),
|
||||||
|
Arguments.of(symlinkAllowedResource, "/symlinkSiblingDir/file", HttpStatus.OK_200, "This file is inside a sibling dir to webroot."),
|
||||||
|
Arguments.of(symlinkAllowedResource, "/webInfSymlink/web.xml", HttpStatus.OK_200, "This is the web.xml file."),
|
||||||
|
|
||||||
|
// The ApproveAliases (approves everything regardless).
|
||||||
|
Arguments.of(approveAliases, "/symlinkFile", HttpStatus.OK_200, "This file is inside webroot."),
|
||||||
|
Arguments.of(approveAliases, "/symlinkExternalFile", HttpStatus.OK_200, "This file is outside webroot."),
|
||||||
|
Arguments.of(approveAliases, "/symlinkDir/file", HttpStatus.OK_200, "This file is inside webroot/documents."),
|
||||||
|
Arguments.of(approveAliases, "/symlinkParentDir/webroot/file", HttpStatus.OK_200, "This file is inside webroot."),
|
||||||
|
Arguments.of(approveAliases, "/symlinkParentDir/webroot/WEB-INF/web.xml", HttpStatus.OK_200, "This is the web.xml file."),
|
||||||
|
Arguments.of(approveAliases, "/symlinkSiblingDir/file", HttpStatus.OK_200, "This file is inside a sibling dir to webroot."),
|
||||||
|
Arguments.of(approveAliases, "/webInfSymlink/web.xml", HttpStatus.OK_200, "This is the web.xml file."),
|
||||||
|
|
||||||
|
// No alias checker (any symlink should be an alias).
|
||||||
|
Arguments.of(null, "/symlinkFile", HttpStatus.NOT_FOUND_404, null),
|
||||||
|
Arguments.of(null, "/symlinkExternalFile", HttpStatus.NOT_FOUND_404, null),
|
||||||
|
Arguments.of(null, "/symlinkDir/file", HttpStatus.NOT_FOUND_404, null),
|
||||||
|
Arguments.of(null, "/symlinkParentDir/webroot/file", HttpStatus.NOT_FOUND_404, null),
|
||||||
|
Arguments.of(null, "/symlinkParentDir/webroot/WEB-INF/web.xml", HttpStatus.NOT_FOUND_404, null),
|
||||||
|
Arguments.of(null, "/symlinkSiblingDir/file", HttpStatus.NOT_FOUND_404, null),
|
||||||
|
Arguments.of(null, "/webInfSymlink/web.xml", HttpStatus.NOT_FOUND_404, null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("testCases")
|
||||||
|
public void test(AliasCheck aliasChecker, String path, int httpStatus, String responseContent) throws Exception
|
||||||
|
{
|
||||||
|
setAliasChecker(aliasChecker);
|
||||||
|
URI uri = URI.create("http://localhost:" + _connector.getLocalPort() + path);
|
||||||
|
ContentResponse response = _client.GET(uri);
|
||||||
|
assertThat(response.getStatus(), is(httpStatus));
|
||||||
|
if (responseContent != null)
|
||||||
|
assertThat(response.getContentAsString(), is(responseContent));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
file 1 contents
|
|
@ -0,0 +1 @@
|
||||||
|
file 2 contents
|
|
@ -0,0 +1 @@
|
||||||
|
This file is outside webroot.
|
|
@ -0,0 +1 @@
|
||||||
|
This file is inside a sibling dir to webroot.
|
|
@ -0,0 +1 @@
|
||||||
|
This is the web.xml file.
|
|
@ -0,0 +1 @@
|
||||||
|
This file is inside webroot/documents.
|
|
@ -0,0 +1 @@
|
||||||
|
This file is inside webroot.
|
|
@ -0,0 +1,4 @@
|
||||||
|
<html>
|
||||||
|
<h1>hello world</h1>
|
||||||
|
<p>body of index.html</p>
|
||||||
|
</html>
|
Loading…
Reference in New Issue