com.phloc.web.servlet.request.RequestHelper Maven / Gradle / Ivy
/**
* Copyright (C) 2006-2015 phloc systems
* http://www.phloc.com
* office[at]phloc[dot]com
*
* Licensed 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 com.phloc.web.servlet.request;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.CheckForSigned;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.phloc.commons.ValueEnforcer;
import com.phloc.commons.annotations.Nonempty;
import com.phloc.commons.annotations.PresentForCodeCoverage;
import com.phloc.commons.annotations.ReturnsMutableCopy;
import com.phloc.commons.lang.GenericReflection;
import com.phloc.commons.string.StringHelper;
import com.phloc.commons.string.StringParser;
import com.phloc.commons.url.URLUtils;
import com.phloc.web.CWeb;
import com.phloc.web.http.CHTTPHeader;
import com.phloc.web.http.EHTTPMethod;
import com.phloc.web.http.EHTTPVersion;
import com.phloc.web.http.HTTPHeaderMap;
import com.phloc.web.port.CNetworkPort;
import com.phloc.web.port.DefaultNetworkPorts;
import jakarta.servlet.http.HttpServletRequest;
/**
* Misc. helper method on {@link HttpServletRequest} objects.
*
* @author Philip Helger, Boris Gregorcic
*/
@Immutable
public final class RequestHelper
{
public static final String SERVLET_ATTR_SSL_CIPHER_SUITE = "jakarta.servlet.request.cipher_suite";
public static final String SERVLET_ATTR_SSL_KEY_SIZE = "jakarta.servlet.request.key_size";
public static final String SERVLET_ATTR_CLIENT_CERTIFICATE = "jakarta.servlet.request.X509Certificate";
private static final String SCOPE_ATTR_REQUESTHELP_REQUESTPARAMMAP = "$requesthelp.requestparammap";
private static final Logger s_aLogger = LoggerFactory.getLogger (RequestHelper.class);
@PresentForCodeCoverage
@SuppressWarnings ("unused")
private static final RequestHelper s_aInstance = new RequestHelper ();
private RequestHelper ()
{}
/**
* Get the passed string without an eventually contained session ID like in
* "test.html;JSESSIONID=1234".
*
* @param sValue
* The value to strip the session ID from
* @return The value without a session ID or the original string.
*/
@Nullable
public static String getWithoutSessionID (@Nonnull final String sValue)
{
ValueEnforcer.notNull (sValue, "Value");
// Strip session ID parameter
final int nIndex = sValue.indexOf (';');
return nIndex == -1 ? sValue : sValue.substring (0, nIndex);
}
/**
* Get the request URI without an eventually appended session
* (";jsessionid=...")
*
* Examples of Returned Values
*
* First line of HTTP request
* Returned Value
*
* POST /some/path.html;JSESSIONID=4711
* /some/path.html
*
*
* GET http://foo.bar/a.html;JSESSIONID=4711
* /a.html
*
*
* HEAD /xyz;JSESSIONID=4711?a=b
* /xyz
*
*
*
* @param aHttpRequest
* The HTTP request
* @return The request URI without the optional session ID
*/
@Nullable
public static String getRequestURI (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
final String sRequestURI = aHttpRequest.getRequestURI ();
if (StringHelper.hasNoText (sRequestURI))
return sRequestURI;
return getWithoutSessionID (sRequestURI);
}
/**
* Get the request path info without an eventually appended session
* (";jsessionid=...")
*
* @param aHttpRequest
* The HTTP request
* @return Returns any extra path information associated with the URL the
* client sent when it made this request. The extra path information
* follows the servlet path but precedes the query string and will
* start with a "/" character. The optional session ID is stripped.
*/
@Nullable
public static String getPathInfo (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
final String sPathInfo = aHttpRequest.getPathInfo ();
if (StringHelper.hasNoText (sPathInfo))
return sPathInfo;
return getWithoutSessionID (sPathInfo);
}
/**
* Return the URI of the request within the servlet context.
*
* @param aHttpRequest
* The HTTP request. May not be null
.
* @return the path within the web application and never null
. By
* default "/" is returned is an empty request URI is determined.
*/
@Nonnull
public static String getPathWithinServletContext (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
final String sRequestURI = getRequestURI (aHttpRequest);
if (StringHelper.hasNoText (sRequestURI))
{
// I just want to to know whether we get null or ""
s_aLogger.info ("Having empty request URI '" + sRequestURI + "' from request " + aHttpRequest);
return "/";
}
final String sContextPath = aHttpRequest.getContextPath ();
final String sPath = getWithoutParentPath (sContextPath, sRequestURI);
if (sPath == null)
return sRequestURI;
// Normal case: URI contains context path.
return sPath.length () > 0 ? sPath : "/";
}
/**
* Checks if the passed parent path is an actual parent path of the passed URL
* path. That is it checks if URLPath starts with ParentPath. In this check,
* the URL path is decoded to also properly match requests where e.g. spaces
* are contained.
*
* @param sParentURLPath
* The URL base path, must not be URLencoded
* @param sURLPath
* The full URL path, must be URLEncoded
* @return the URL path (still encoded in the same way as passed) where the
* parent path has been trimmed (matching the decoded URL path), or
* null
if the URL path does not start with that parent
* path
*/
@Nullable
public static String getWithoutParentPath (final String sParentURLPath, final String sURLPath)
{
if (StringHelper.hasNoText (sParentURLPath) || StringHelper.hasNoText (sURLPath))
{
return null;
}
final boolean bDelimiterAdd = !StringHelper.endsWith (sParentURLPath, '/');
final String sParentPathToUse = sParentURLPath + (bDelimiterAdd ? "/" : "");
final String sDecodedURLPath = URLUtils.urlDecode (sURLPath);
if (StringHelper.startsWith (sDecodedURLPath, sParentPathToUse))
{
// go only for the slashes to determine trailing part, as the encoding and
// decoding can never reproduces without loosing information!
final int nParentEnd = StringHelper.getOccurrenceCount (sParentPathToUse, '/');
String sPath = sURLPath;
// TODO BG: extract to phloc-commons
for (int i = 0; i < nParentEnd; i++)
{
sPath = StringHelper.getFromFirstExcl (sPath, '/');
}
return (bDelimiterAdd ? "/" : "") + sPath;
}
return null;
}
/**
* Return the path within the servlet mapping for the given request, i.e. the
* part of the request's URL beyond the part that called the servlet, or "" if
* the whole URL has been used to identify the servlet.
*
* Detects include request URL if called within a RequestDispatcher include.
*
* E.g.: servlet mapping = "/test/*"; request URI = "/test/a" -> "/a".
*
* E.g.: servlet mapping = "/test"; request URI = "/test" -> "".
*
* E.g.: servlet mapping = "/*.test"; request URI = "/a.test" -> "".
*
* @param aHttpRequest
* current HTTP request
* @return the path within the servlet mapping, or ""
*/
@Nonnull
public static String getPathWithinServlet (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
final String sPathWithinApp = getPathWithinServletContext (aHttpRequest);
final String sServletPath = aHttpRequest.getServletPath ();
if (sPathWithinApp.startsWith (sServletPath))
return sPathWithinApp.substring (sServletPath.length ());
// Special case: URI is different from servlet path.
// Can happen e.g. with index page: URI="/", servletPath="/index.html"
// Use servlet path in this case, as it indicates the actual target path.
return sServletPath;
}
/**
* Get the full URL (incl. protocol) and parameters of the passed request.
*
*
* http://hostname.com/mywebapp/servlet/MyServlet/a/b;c=123?d=789
*
*
* @param aHttpRequest
* The request to use. May not be null
.
* @return The full URL.
*/
@Nonnull
@Nonempty
public static String getURL (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
final StringBuffer aReqUrl = aHttpRequest.getRequestURL ();
final String sQueryString = aHttpRequest.getQueryString (); // d=789
if (sQueryString != null)
aReqUrl.append (URLUtils.QUESTIONMARK).append (sQueryString);
return aReqUrl.toString ();
}
/**
* Get the full URI (excl. protocol) and parameters of the passed request.
* Example:
*
*
* /mywebapp/servlet/MyServlet/a/b;c=123?d=789
*
*
* @param aHttpRequest
* The request to use. May not be null
.
* @return The full URI.
*/
@Nonnull
@Nonempty
public static String getURI (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
final String sReqUrl = getRequestURI (aHttpRequest);
final String sQueryString = aHttpRequest.getQueryString (); // d=789&x=y
if (StringHelper.hasText (sQueryString))
return sReqUrl + URLUtils.QUESTIONMARK + sQueryString;
return sReqUrl;
}
@CheckForSigned
public static int getDefaultServerPort (@Nullable final String sScheme)
{
if (CWeb.SCHEME_HTTP.equalsIgnoreCase (sScheme))
return CWeb.DEFAULT_PORT_HTTP;
if (CWeb.SCHEME_HTTPS.equalsIgnoreCase (sScheme))
return CWeb.DEFAULT_PORT_HTTPS;
return CNetworkPort.INVALID_PORT_NUMBER;
}
@CheckForSigned
public static int getServerPortToUse (@Nonnull final String sScheme, @CheckForSigned final int nServerPort)
{
// URL.getPort() delivers -1 for unspecified ports
if (!DefaultNetworkPorts.isValidPort (nServerPort))
return getDefaultServerPort (sScheme);
return nServerPort;
}
@Nonnull
@Nonempty
public static StringBuilder getFullServerName (@Nonnull final String sScheme,
@Nonnull final String sServerName,
final int nServerPort)
{
ValueEnforcer.notNull (sScheme, "Scheme");
ValueEnforcer.notNull (sServerName, "ServerName");
// Reconstruct URL
final StringBuilder aSB = new StringBuilder (sScheme).append ("://").append (sServerName);
if (DefaultNetworkPorts.isValidPort (nServerPort) && nServerPort != getDefaultServerPort (sScheme))
aSB.append (':').append (nServerPort);
return aSB;
}
@Nonnull
@Nonempty
public static String getFullServerNameAndPath (@Nonnull final String sScheme,
@Nonnull final String sServerName,
final int nServerPort,
@Nullable final String sPath,
@Nullable final String sQueryString)
{
final StringBuilder aURL = getFullServerName (sScheme, sServerName, nServerPort);
if (StringHelper.hasText (sPath))
{
if (!sPath.startsWith ("/", 0))
aURL.append ('/');
aURL.append (sPath);
}
if (StringHelper.hasText (sQueryString))
aURL.append (URLUtils.QUESTIONMARK).append (sQueryString);
return aURL.toString ();
}
@Nullable
public static String getHttpReferer (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
return aHttpRequest.getHeader (CHTTPHeader.REFERER);
}
/**
* Get the HTTP version associated with the given HTTP request
*
* @param aHttpRequest
* The http request to query. May not be null
.
* @return null
if no supported HTTP version is contained
*/
@Nullable
public static EHTTPVersion getHttpVersion (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
final String sProtocol = aHttpRequest.getProtocol ();
return EHTTPVersion.getFromNameOrNull (sProtocol);
}
/**
* Get the HTTP method associated with the given HTTP request
*
* @param aHttpRequest
* The http request to query. May not be null
.
* @return null
if no supported HTTP method is contained
*/
@Nullable
public static EHTTPMethod getHttpMethod (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
final String sMethod = aHttpRequest.getMethod ();
return EHTTPMethod.getFromNameOrNull (sMethod);
}
/**
* Get a complete request header map as a copy.
*
* @param aHttpRequest
* The source HTTP request. May not be null
.
* @return Never null
.
*/
@Nonnull
@ReturnsMutableCopy
public static HTTPHeaderMap getRequestHeaderMap (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
final HTTPHeaderMap ret = new HTTPHeaderMap ();
final Enumeration > eHeaders = aHttpRequest.getHeaderNames ();
while (eHeaders.hasMoreElements ())
{
final String sName = (String) eHeaders.nextElement ();
final Enumeration > eHeaderValues = aHttpRequest.getHeaders (sName);
while (eHeaderValues.hasMoreElements ())
{
final String sValue = (String) eHeaderValues.nextElement ();
ret.addHeader (sName, sValue);
}
}
return ret;
}
/**
* This is a utility method which avoids that all map values are enclosed in
* an array. Jetty seems to create String arrays out of simple string values
*
* @param aHttpRequest
* The source HTTP request. May not be null
.
* @return A Map containing pure strings instead of string arrays with one
* item
*/
@Nonnull
@ReturnsMutableCopy
public static Map getParameterMap (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest"); //$NON-NLS-1$
final Map aResult = new HashMap <> ();
@SuppressWarnings ("unchecked")
final Map aOriginalMap = aHttpRequest.getParameterMap ();
// For all parameters
for (final Map.Entry aEntry : aOriginalMap.entrySet ())
{
final String sKey = aEntry.getKey ();
final String [] aValue = aEntry.getValue ();
if (aValue.length > 1)
aResult.put (sKey, aValue);
else
if (aValue.length == 1)
{
// Flatten array to String
aResult.put (sKey, aValue[0]);
}
else
aResult.put (sKey, ""); //$NON-NLS-1$
}
return aResult;
}
@Nonnull
public static IRequestParamMap getRequestParamMap (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest"); //$NON-NLS-1$
// Check if a value is cached in the HTTP request
IRequestParamMap aValue = (IRequestParamMap) aHttpRequest.getAttribute (SCOPE_ATTR_REQUESTHELP_REQUESTPARAMMAP);
if (aValue == null)
{
aValue = RequestParamMap.create (getParameterMap (aHttpRequest));
aHttpRequest.setAttribute (SCOPE_ATTR_REQUESTHELP_REQUESTPARAMMAP, aValue);
}
return aValue;
}
/**
* Get the content length of the passed request. This is not done using
* request.getContentLength()
but instead parsing the HTTP header
* field {@link CHTTPHeader#CONTENT_LENGTH} manually!
*
* @param aHttpRequest
* Source HTTP request. May not be null
.
* @return -1 if no or an invalid content length is set in the header
*/
@CheckForSigned
public static long getContentLength (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
// Missing support > 2GB!!!
// return aHttpRequest.getContentLength ();
final String sContentLength = aHttpRequest.getHeader (CHTTPHeader.CONTENT_LENGTH);
return StringParser.parseLong (sContentLength, -1L);
}
/**
* Get all request headers of the passed request in a correctly typed
* {@link Enumeration}.
*
* @param aHttpRequest
* Source HTTP request. May not be null
.
* @param sName
* Name of the request header to retrieve.
* @return Never null
.
*/
@Nullable
public static Enumeration getRequestHeaders (@Nonnull final HttpServletRequest aHttpRequest,
@Nullable final String sName)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
return GenericReflection., Enumeration > uncheckedCast (aHttpRequest.getHeaders (sName));
}
/**
* Get all all request header names of the passed request in a correctly typed
* {@link Enumeration}.
*
* @param aHttpRequest
* Source HTTP request. May not be null
.
* @return Never null
.
*/
@Nullable
public static Enumeration getRequestHeaderNames (@Nonnull final HttpServletRequest aHttpRequest)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
return GenericReflection., Enumeration > uncheckedCast (aHttpRequest.getHeaderNames ());
}
@Nullable
private static T _getRequestAttr (@Nonnull final HttpServletRequest aHttpRequest,
@Nonnull @Nonempty final String sAttrName,
@Nonnull final Class aDstClass)
{
ValueEnforcer.notNull (aHttpRequest, "HttpRequest");
final Object aValue = aHttpRequest.getAttribute (sAttrName);
if (aValue == null)
{
// No client certificates present
return null;
}
// type check
if (!aDstClass.isAssignableFrom (aValue.getClass ()))
{
s_aLogger.error ("Request attribute " +
sAttrName +
" is not of type " +
aDstClass.getName () +
" but of type " +
aValue.getClass ().getName ());
return null;
}
// Return the certificates
return aDstClass.cast (aValue);
}
/**
* @param aHttpRequest
* he HTTP servlet request to extract the information from. May not be
* null
.
* @return SSL cipher suite or null
if no such attribute is
* present
*/
@Nullable
public static String getRequestSSLCipherSuite (@Nonnull final HttpServletRequest aHttpRequest)
{
return _getRequestAttr (aHttpRequest, SERVLET_ATTR_SSL_CIPHER_SUITE, String.class);
}
/**
* @param aHttpRequest
* he HTTP servlet request to extract the information from. May not be
* null
.
* @return Bit size of the algorithm or null
if no such attribute
* is present
*/
@Nullable
public static Integer getRequestSSLKeySize (@Nonnull final HttpServletRequest aHttpRequest)
{
return _getRequestAttr (aHttpRequest, SERVLET_ATTR_SSL_KEY_SIZE, Integer.class);
}
/**
* Get the client certificates provided by a HTTP servlet request.
*
* @param aHttpRequest
* The HTTP servlet request to extract the information from. May not be
* null
.
* @return null
if the passed request does not contain any client
* certificate
*/
@Nullable
public static X509Certificate [] getRequestClientCertificates (@Nonnull final HttpServletRequest aHttpRequest)
{
return _getRequestAttr (aHttpRequest, SERVLET_ATTR_CLIENT_CERTIFICATE, X509Certificate [].class);
}
}