blob: 894ceab9518027d03fc6db12df6b1f61ab1c97d1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sling.engine.impl.request;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestWrapper;
import javax.servlet.ServletResponse;
import javax.servlet.ServletResponseWrapper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RecursionTooDeepException;
import org.apache.sling.api.request.RequestPathInfo;
import org.apache.sling.api.request.RequestProgressTracker;
import org.apache.sling.api.request.RequestUtil;
import org.apache.sling.api.request.TooManyCallsException;
import org.apache.sling.api.request.builder.Builders;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.ServletResolver;
import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
import org.apache.sling.api.wrappers.SlingHttpServletResponseWrapper;
import org.apache.sling.engine.impl.SlingHttpServletRequestImpl;
import org.apache.sling.engine.impl.SlingHttpServletResponseImpl;
import org.apache.sling.engine.impl.SlingMainServlet;
import org.apache.sling.engine.impl.SlingRequestProcessorImpl;
import org.apache.sling.engine.impl.adapter.SlingServletRequestAdapter;
import org.apache.sling.engine.impl.adapter.SlingServletResponseAdapter;
import org.apache.sling.engine.impl.parameters.ParameterSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.sling.api.SlingConstants.SLING_CURRENT_SERVLET_NAME;
/**
* The <code>RequestData</code> class provides access to objects which are set
* on a Servlet Request wide basis such as the repository session, the
* persistence manager, etc.
*
* The setup order is:
* <ol>
* <li>Invoke constructor</li>
* <li>Invoke initResource()</li>
* <li>Invoke initServlet()</li>
* </ol>
* @see ContentData
*/
public class RequestData {
/** Logger - use static as a new object is created with every request */
private static final Logger log = LoggerFactory.getLogger(RequestData.class);
/**
* The name of the request attribute providing the resource addressed by the
* request URL.
*/
public static final String REQUEST_RESOURCE_PATH_ATTR = "$$sling.request.resource$$";
/**
* The name of the request attribute to override the max call number (-1 for infinite or integer value).
*/
private static String REQUEST_MAX_CALL_OVERRIDE = "sling.max.calls";
/** The request processor used for request dispatching and other stuff */
private final SlingRequestProcessorImpl slingRequestProcessor;
private final long startTimestamp;
/** The original servlet Servlet Request Object */
private final HttpServletRequest servletRequest;
/** The original servlet Servlet Response object */
private final HttpServletResponse servletResponse;
/** The original servlet Servlet Request Object */
private final SlingHttpServletRequest slingRequest;
/** The original servlet Servlet Response object */
private final SlingHttpServletResponse slingResponse;
private final boolean protectHeadersOnInclude;
private final boolean checkContentTypeOnInclude;
/** The parameter support class */
private ParameterSupport parameterSupport;
private ResourceResolver resourceResolver;
private final RequestProgressTracker requestProgressTracker;
/** the current ContentData */
private ContentData currentContentData;
/**
* the number of servlets called by
* {@link #service(SlingHttpServletRequest, SlingHttpServletResponse)}
*/
private int servletCallCounter;
/**
* The name of the currently active serlvet.
*
* @see #setActiveServletName(String)
* @see #getActiveServletName()
*/
private String activeServletName;
/**
* Recursion depth
*/
private int recursionDepth;
/**
* The peak value for the recursion depth.
*/
private int peakRecusionDepth;
/**
* Current dispatching info
*/
private DispatchingInfo dispatchingInfo;
private final boolean disableCheckCompliantGetUserPrincipal;
private static volatile boolean loggedNonCompliantGetUserPrincipalWarning = false;
/**
* Prevent traversal using '/../' or '/..' even if '[' or '}' is used in-between
*/
private static final Set<Character> SKIPPED_TRAVERSAL_CHARS = new HashSet<>();
static {
SKIPPED_TRAVERSAL_CHARS.add('[');
SKIPPED_TRAVERSAL_CHARS.add('}');
}
public RequestData(
SlingRequestProcessorImpl slingRequestProcessor,
HttpServletRequest request,
HttpServletResponse response,
boolean protectHeadersOnInclude,
boolean checkContentTypeOnInclude,
boolean disableCheckCompliantGetUserPrincipal) {
this.startTimestamp = System.currentTimeMillis();
this.slingRequestProcessor = slingRequestProcessor;
this.servletRequest = request;
this.servletResponse = response;
this.protectHeadersOnInclude = protectHeadersOnInclude;
this.checkContentTypeOnInclude = checkContentTypeOnInclude;
this.disableCheckCompliantGetUserPrincipal = disableCheckCompliantGetUserPrincipal;
this.slingRequest = new SlingHttpServletRequestImpl(this, this.servletRequest);
this.slingResponse = new SlingHttpServletResponseImpl(this, this.servletResponse);
// Use tracker from SlingHttpServletRequest
if (request instanceof SlingHttpServletRequest) {
this.requestProgressTracker = ((SlingHttpServletRequest) request).getRequestProgressTracker();
} else {
// Getting the RequestProgressTracker from the request attributes like
// this should not be generally used, it's just a way to pass it from
// its creation point to here, so it's made available via
// the Sling request's getRequestProgressTracker method.
final Object o = request.getAttribute(RequestProgressTracker.class.getName());
if (o instanceof RequestProgressTracker) {
this.requestProgressTracker = (RequestProgressTracker) o;
} else {
log.warn("RequestProgressTracker not found in request attributes");
this.requestProgressTracker = Builders.newRequestProgressTracker();
this.requestProgressTracker.log("Method={0}, PathInfo={1}", request.getMethod(), request.getPathInfo());
}
}
}
public Resource initResource(ResourceResolver resourceResolver) {
// keep the resource resolver for request processing
this.resourceResolver = resourceResolver;
// resolve the resource
requestProgressTracker.startTimer("ResourceResolution");
final SlingHttpServletRequest request = getSlingRequest();
StringBuffer requestURL = servletRequest.getRequestURL();
String path = request.getPathInfo();
if (requestURL.indexOf(";") > -1 && !path.contains(";")) {
try {
final URL rUrl = new URL(requestURL.toString());
final String prefix = request.getContextPath().concat(request.getServletPath());
path = rUrl.getPath().substring(prefix.length());
} catch (final MalformedURLException e) {
// ignore
}
}
Resource resource = resourceResolver.resolve(request, path);
if (request.getAttribute(REQUEST_RESOURCE_PATH_ATTR) == null) {
request.setAttribute(REQUEST_RESOURCE_PATH_ATTR, resource.getPath());
}
requestProgressTracker.logTimer(
"ResourceResolution",
"URI={0} resolves to Resource={1}",
getServletRequest().getRequestURI(),
resource);
return resource;
}
public void initServlet(final Resource resource, final ServletResolver sr) {
// the resource and the request path info, will never be null
RequestPathInfo requestPathInfo = new SlingRequestPathInfo(resource);
ContentData contentData = setContent(resource, requestPathInfo);
requestProgressTracker.log("Resource Path Info: {0}", requestPathInfo);
// finally resolve the servlet for the resource
requestProgressTracker.startTimer("ServletResolution");
Servlet servlet = sr.resolveServlet(slingRequest);
requestProgressTracker.logTimer(
"ServletResolution",
"URI={0} handled by Servlet={1}",
getServletRequest().getRequestURI(),
(servlet == null ? "-none-" : RequestUtil.getServletName(servlet)));
contentData.setServlet(servlet);
}
public SlingRequestProcessorImpl getSlingRequestProcessor() {
return slingRequestProcessor;
}
public HttpServletRequest getServletRequest() {
return servletRequest;
}
public HttpServletResponse getServletResponse() {
return servletResponse;
}
public SlingHttpServletRequest getSlingRequest() {
return slingRequest;
}
public SlingHttpServletResponse getSlingResponse() {
return slingResponse;
}
public DispatchingInfo getDispatchingInfo() {
return dispatchingInfo;
}
public void setDispatchingInfo(final DispatchingInfo dispatchingInfo) {
this.dispatchingInfo = dispatchingInfo;
}
// ---------- Request Helper
/**
* Unwraps the ServletRequest to a SlingHttpServletRequest.
*
* @param request the request
* @return the unwrapped request
* @throws IllegalArgumentException If the <code>request</code> is not a
* <code>SlingHttpServletRequest</code> and not a
* <code>ServletRequestWrapper</code> wrapping a
* <code>SlingHttpServletRequest</code>.
*/
public static SlingHttpServletRequest unwrap(ServletRequest request) {
// early check for most cases
if (request instanceof SlingHttpServletRequest) {
return (SlingHttpServletRequest) request;
}
// unwrap wrappers
while (request instanceof ServletRequestWrapper) {
request = ((ServletRequestWrapper) request).getRequest();
// immediate termination if we found one
if (request instanceof SlingHttpServletRequest) {
return (SlingHttpServletRequest) request;
}
}
// if we unwrapped everything and did not find a
// SlingHttpServletRequest, we lost
throw new IllegalArgumentException("ServletRequest not wrapping SlingHttpServletRequest");
}
/**
* Unwraps the SlingHttpServletRequest to a SlingHttpServletRequestImpl
*
* @param request the request
* @return the unwrapped request
* @throws IllegalArgumentException If <code>request</code> is not a
* <code>SlingHttpServletRequestImpl</code> and not
* <code>SlingHttpServletRequestWrapper</code> wrapping a
* <code>SlingHttpServletRequestImpl</code>.
*/
public static SlingHttpServletRequestImpl unwrap(SlingHttpServletRequest request) {
while (request instanceof SlingHttpServletRequestWrapper) {
request = ((SlingHttpServletRequestWrapper) request).getSlingRequest();
}
if (request instanceof SlingHttpServletRequestImpl) {
return (SlingHttpServletRequestImpl) request;
}
throw new IllegalArgumentException("SlingHttpServletRequest not of correct type");
}
/**
* Unwraps the ServletRequest to a SlingHttpServletRequest.
*
* @param response the response
* @return the unwrapped response
* @throws IllegalArgumentException If the <code>response</code> is not a
* <code>SlingHttpServletResponse</code> and not a
* <code>ServletResponseWrapper</code> wrapping a
* <code>SlingHttpServletResponse</code>.
*/
public static SlingHttpServletResponse unwrap(ServletResponse response) {
// early check for most cases
if (response instanceof SlingHttpServletResponse) {
return (SlingHttpServletResponse) response;
}
// unwrap wrappers
while (response instanceof ServletResponseWrapper) {
response = ((ServletResponseWrapper) response).getResponse();
// immediate termination if we found one
if (response instanceof SlingHttpServletResponse) {
return (SlingHttpServletResponse) response;
}
}
// if we unwrapped everything and did not find a
// SlingHttpServletResponse, we lost
throw new IllegalArgumentException("ServletResponse not wrapping SlingHttpServletResponse");
}
/**
* Unwraps a SlingHttpServletResponse to a SlingHttpServletResponseImpl
*
* @param response the response
* @return the unwrapped response
* @throws IllegalArgumentException If <code>response</code> is not a
* <code>SlingHttpServletResponseImpl</code> and not
* <code>SlingHttpServletResponseWrapper</code> wrapping a
* <code>SlingHttpServletResponseImpl</code>.
*/
public static SlingHttpServletResponseImpl unwrap(SlingHttpServletResponse response) {
while (response instanceof SlingHttpServletResponseWrapper) {
response = ((SlingHttpServletResponseWrapper) response).getSlingResponse();
}
if (response instanceof SlingHttpServletResponseImpl) {
return (SlingHttpServletResponseImpl) response;
}
throw new IllegalArgumentException("SlingHttpServletResponse not of correct type");
}
/**
* @param request the request
* @return the request data
* @throws IllegalArgumentException If the <code>request</code> is not a
* <code>SlingHttpServletRequest</code> and not a
* <code>ServletRequestWrapper</code> wrapping a
* <code>SlingHttpServletRequest</code>.
*/
public static RequestData getRequestData(ServletRequest request) {
return unwrap(unwrap(request)).getRequestData();
}
/**
* @param request the request
* @return the request data
* @throws IllegalArgumentException If <code>request</code> is not a
* <code>SlingHttpServletRequestImpl</code> and not
* <code>SlingHttpServletRequestWrapper</code> wrapping a
* <code>SlingHttpServletRequestImpl</code>.
*/
public static RequestData getRequestData(SlingHttpServletRequest request) {
return unwrap(request).getRequestData();
}
/**
* @param request the request
* @return the sling http servlet request
* @throws IllegalArgumentException if <code>request</code> is not a
* <code>HttpServletRequest</code> of if <code>request</code>
* is not backed by <code>SlingHttpServletRequestImpl</code>.
*/
public static SlingHttpServletRequest toSlingHttpServletRequest(ServletRequest request) {
// unwrap to SlingHttpServletRequest, may throw if no
// SlingHttpServletRequest is wrapped in request
SlingHttpServletRequest cRequest = unwrap(request);
// ensure the SlingHttpServletRequest is backed by
// SlingHttpServletRequestImpl
RequestData.unwrap(cRequest);
// if the request is not wrapper at all, we are done
if (cRequest == request) {
return cRequest;
}
// ensure the request is a HTTP request
if (!(request instanceof HttpServletRequest)) {
throw new IllegalArgumentException("Request is not an HTTP request");
}
// otherwise, we create a new response wrapping the servlet response
// and unwrapped component response
return new SlingServletRequestAdapter(cRequest, (HttpServletRequest) request);
}
/**
* @param response the response
* @return the sling http servlet response
* @throws IllegalArgumentException if <code>response</code> is not a
* <code>HttpServletResponse</code> of if
* <code>response</code> is not backed by
* <code>SlingHttpServletResponseImpl</code>.
*/
public static SlingHttpServletResponse toSlingHttpServletResponse(ServletResponse response) {
// unwrap to SlingHttpServletResponse
SlingHttpServletResponse cResponse = unwrap(response);
// if the servlet response is actually the SlingHttpServletResponse, we
// are done
if (cResponse == response) {
return cResponse;
}
// ensure the response is a HTTP response
if (!(response instanceof HttpServletResponse)) {
throw new IllegalArgumentException("Response is not an HTTP response");
}
// otherwise, we create a new response wrapping the servlet response
// and unwrapped component response
return new SlingServletResponseAdapter(cResponse, (HttpServletResponse) response);
}
/**
* Helper method to call the servlet for the current content data. If the
* current content data has no servlet, <em>NOT_FOUND</em> (404) error is
* sent and the method terminates.
* <p>
* If the the servlet exists, the
* {@link org.apache.sling.api.SlingConstants#SLING_CURRENT_SERVLET_NAME} request attribute is set
* to the name of that servlet and that servlet name is also set as the
* {@link #setActiveServletName(String) currently active servlet}. After
* the termination of the servlet (normal or throwing a Throwable) the
* request attribute is reset to the previous value. The name of the
* currently active servlet is only reset to the previous value if the
* servlet terminates normally. In case of a Throwable, the active servlet
* name is not reset and indicates which servlet caused the potential abort
* of the request.
*
* @param request The request object for the service method
* @param response The response object for the service method
* @throws IOException May be thrown by the servlet's service method
* @throws ServletException May be thrown by the servlet's service method
*/
public static void service(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws IOException, ServletException {
if (!isValidRequest(
request.getRequestPathInfo().getResourcePath(),
request.getRequestPathInfo().getSelectors())) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Malformed request syntax");
return;
}
RequestData requestData = RequestData.getRequestData(request);
Servlet servlet = requestData.getContentData().getServlet();
if (servlet == null) {
log.warn(
"Did not find a servlet to handle the request (path={},selectors={},extension={},suffix={})",
request.getRequestPathInfo().getResourcePath(),
Arrays.toString(request.getRequestPathInfo().getSelectors()),
request.getRequestPathInfo().getExtension(),
request.getRequestPathInfo().getSuffix());
response.sendError(HttpServletResponse.SC_NOT_FOUND, "No Servlet to handle request");
} else {
String name = RequestUtil.getServletName(servlet);
// verify the number of service calls in this request
if (requestData.hasServletMaxCallCount(request)) {
throw new TooManyCallsException(name);
}
// replace the current servlet name in the request
Object oldValue = request.getAttribute(SLING_CURRENT_SERVLET_NAME);
request.setAttribute(SLING_CURRENT_SERVLET_NAME, name);
// setup the tracker for this service call
String timerName = name + "#" + requestData.servletCallCounter;
requestData.servletCallCounter++;
requestData.getRequestProgressTracker().startTimer(timerName);
String prevServletName = requestData.setActiveServletName(name);
try {
servlet.service(request, response);
} finally {
requestData.setActiveServletName(prevServletName);
request.setAttribute(SLING_CURRENT_SERVLET_NAME, oldValue);
requestData.getRequestProgressTracker().logTimer(timerName);
}
}
}
/*
* Don't allow path segments that contain only dots or a mix of dots and %5B.
* Additionally, check that we didn't have an empty selector from a dot replacement.
*/
static boolean isValidRequest(String resourcePath, String... selectors) {
for (String selector : selectors) {
if (selector.trim().isEmpty()) {
return false;
}
}
return resourcePath == null || !traversesParentPath(resourcePath);
}
// ---------- Content inclusion stacking -----------------------------------
public ContentData setContent(final Resource resource, final RequestPathInfo requestPathInfo) {
if (this.recursionDepth >= this.slingRequestProcessor.getMaxIncludeCounter()) {
throw new RecursionTooDeepException(requestPathInfo.getResourcePath());
}
this.recursionDepth++;
if (this.recursionDepth > this.peakRecusionDepth) {
this.peakRecusionDepth = this.recursionDepth;
}
currentContentData = new ContentData(resource, requestPathInfo);
return currentContentData;
}
public void resetContent(final ContentData data) {
this.recursionDepth--;
currentContentData = data;
}
public ContentData getContentData() {
return currentContentData;
}
public ResourceResolver getResourceResolver() {
return resourceResolver;
}
public RequestProgressTracker getRequestProgressTracker() {
return requestProgressTracker;
}
public int getPeakRecusionDepth() {
return peakRecusionDepth;
}
public int getServletCallCount() {
return servletCallCounter;
}
public boolean protectHeadersOnInclude() {
return protectHeadersOnInclude;
}
public boolean checkContentTypeOnInclude() {
return checkContentTypeOnInclude;
}
/**
* Returns {@code true} if the number of {@code RequestDispatcher.include}
* calls has been reached within the given request. That maximum number may
* either be defined by the {@link #REQUEST_MAX_CALL_OVERRIDE} request
* attribute or the {@link SlingMainServlet#PROP_MAX_CALL_COUNTER}
* configuration of the {@link SlingMainServlet}.
*
* @param request The request to check
* @return {@code true} if the maximum number of calls has been reached (or
* surpassed)
*/
private boolean hasServletMaxCallCount(final ServletRequest request) {
// verify the number of service calls in this request
log.debug("Servlet call counter : {}", getServletCallCount());
// max number of calls can be overriden with a request attribute (-1 for
// infinite or integer value)
int maxCallCounter = this.slingRequestProcessor.getMaxCallCounter();
Object reqMaxOverride = request.getAttribute(REQUEST_MAX_CALL_OVERRIDE);
if (reqMaxOverride instanceof Number) {
maxCallCounter = ((Number) reqMaxOverride).intValue();
}
return (maxCallCounter >= 0) && getServletCallCount() >= maxCallCounter;
}
public long getElapsedTimeMsec() {
return System.currentTimeMillis() - startTimestamp;
}
/**
* Sets the name of the currently active servlet and returns the name of the
* previously active servlet.
*
* @param servletName the servlet name
* @return the previous servlet name
*/
public String setActiveServletName(String servletName) {
String old = activeServletName;
activeServletName = servletName;
return old;
}
/**
* Returns the name of the currently active servlet. If this name is not
* <code>null</code> at the end of request processing, more precisly in
* the case of an uncaught <code>Throwable</code> at the end of request
* processing, this is the name of the servlet causing the uncaught
* <code>Throwable</code>.
*
* @return the current servlet name
*/
public String getActiveServletName() {
return activeServletName;
}
// ---------- Parameter support -------------------------------------------
public ServletInputStream getInputStream() throws IOException {
if (parameterSupport != null && parameterSupport.requestDataUsed()) {
throw new IllegalStateException("Request Data has already been read");
}
// may throw IllegalStateException if the reader has already been
// acquired
return getServletRequest().getInputStream();
}
public BufferedReader getReader() throws UnsupportedEncodingException, IOException {
if (parameterSupport != null && parameterSupport.requestDataUsed()) {
throw new IllegalStateException("Request Data has already been read");
}
// may throw IllegalStateException if the input stream has already been
// acquired
return getServletRequest().getReader();
}
public ParameterSupport getParameterSupport() {
if (parameterSupport == null) {
parameterSupport = ParameterSupport.getInstance(getServletRequest());
}
return parameterSupport;
}
/*
* Traverses the path segment wise and checks
* if there is any path with only dots (".")
* skipping SKIPPED_TRAVERSAL_CHARS characters in segment.
*/
private static boolean traversesParentPath(String path) {
int index = 0;
while (index < path.length()) {
int charCount = 0;
int dotCount = 0;
// count dots (".") and total chars in each path segment (between two '/')
while (index < path.length() && path.charAt(index) != '/') {
char c = path.charAt(index);
if (!SKIPPED_TRAVERSAL_CHARS.contains(c)) {
if (c == '.') {
dotCount++;
}
charCount++;
}
index++;
}
// if all chars are dots (".")
// path is traversing the parent directory
if (charCount > 1 && dotCount == charCount) {
return true;
}
index++;
}
return false;
}
public boolean isDisableCheckCompliantGetUserPrincipal() {
return disableCheckCompliantGetUserPrincipal;
}
public void logNonCompliantGetUserPrincipalWarning() {
if (!loggedNonCompliantGetUserPrincipalWarning) {
loggedNonCompliantGetUserPrincipalWarning = true;
log.warn(
"Request.getUserPrincipal() called without a remoteUser set. This is not compliant to the servlet spec "
+ "and might return a principal even if the request is not authenticated. Please update your code to use getAuthType() "
+ "to check for anonymous requests first.");
}
}
}