blob: 93e73168c14defc5fb08456c7ab32ad46ab21305 [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.auth.core.impl;
import java.io.Closeable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.sling.api.SlingConstants;
import org.apache.sling.api.auth.Authenticator;
import org.apache.sling.api.auth.NoAuthenticationHandlerException;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.auth.core.AuthConstants;
import org.apache.sling.auth.core.AuthUtil;
import org.apache.sling.auth.core.AuthenticationSupport;
import org.apache.sling.auth.core.LoginEventDecorator;
import org.apache.sling.auth.core.spi.AuthenticationFeedbackHandler;
import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.apache.sling.auth.core.spi.AuthenticationHandler.FAILURE_REASON_CODES;
import org.apache.sling.auth.core.spi.AuthenticationInfo;
import org.apache.sling.auth.core.spi.AuthenticationInfoPostProcessor;
import org.apache.sling.auth.core.spi.DefaultAuthenticationFeedbackHandler;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.FieldOption;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.component.annotations.ReferencePolicyOption;
import org.osgi.service.component.propertytypes.ServiceDescription;
import org.osgi.service.component.propertytypes.ServiceVendor;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.osgi.service.http.context.ServletContextHelper;
import org.osgi.service.http.whiteboard.HttpWhiteboardConstants;
import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardContextSelect;
import org.osgi.service.http.whiteboard.propertytypes.HttpWhiteboardListener;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.osgi.service.metatype.annotations.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>SlingAuthenticator</code> class is the default implementation for
* handling authentication. This class supports :
* <ul>
* <li>Support for login sessions where session ids are exchanged with cookies
* <li>Support for multiple authentication handlers, which must implement the
* {@link AuthenticationHandler} interface.
* <li>
* </ul>
* <p>
* Currently this class does not support multiple handlers for any one request
* URL.
*/
@Component(name = SlingAuthenticator.PID,
service = {Authenticator.class, AuthenticationSupport.class, ServletRequestListener.class})
@HttpWhiteboardContextSelect("(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=*)")
@HttpWhiteboardListener
@ServiceDescription("Apache Sling Request Authenticator")
@ServiceVendor("The Apache Software Foundation")
@Designate(ocd = SlingAuthenticator.Config.class)
public class SlingAuthenticator implements Authenticator,
AuthenticationSupport, ServletRequestListener {
/** The pid for the configuration */
public static final String PID = "org.apache.sling.engine.impl.auth.SlingAuthenticator";
@ObjectClassDefinition(name = "Apache Sling Authentication Service",
description = "Extracts user authentication details from the request with" +
" the help of authentication handlers registered as separate services. One" +
" example of such an authentication handler is the handler HTTP Authorization" +
" header contained authentication.")
public @interface Config {
@AttributeDefinition(name = "Impersonation Cookie",
description = "The name the HTTP Cookie to set with the value" +
" of the user which is to be impersonated. This cookie will always be a session" +
" cookie.")
String auth_sudo_cookie() default "sling.sudo"; // NOSONAR
@AttributeDefinition(name = "Impersonation Parameter",
description = "The name of the request parameter initiating" +
" impersonation. Setting this parameter to a user id will result in using an" +
" impersonated session (instead of the actually authenticated session) and set" +
" a session cookie of the name defined in the Impersonation Cookie setting.")
String auth_sudo_parameter() default "sudo"; // NOSONAR
@AttributeDefinition(name = "Allow Anonymous Access",
description = "Whether default access as anonymous when no" +
" credentials are present in the request is allowed. The default value is" +
" \"true\" to allow access without credentials. When set to \"false\" access to the" +
" repository is only allowed if valid credentials are presented. The value of" +
" this configuration option is added to list of Authentication Requirements" +
" and needs not be explicitly listed. If anonymous access is allowed the entry" +
" added is \"-/\". Otherwise anonymous access is denied and \"+/\" is added to the" +
" list.")
boolean auth_annonymous() default true; // NOSONAR
@AttributeDefinition(name = "Authentication Requirements",
description = "Defines URL space subtrees which require" +
" or don't require authentication. For any request the best matching path" +
" configured applies and defines whether authentication is actually required" +
" for the request or not. Each entry in this list can be an absolute path (such" +
" as /content) or and absolute URI (such as http://thehost/content). Optionally" +
" each entry may be prefixed by a plus (+) or minus (-) sign indicating that" +
" authentication is required (plus) or not required (minus). Example entries are" +
" \"/content\" or \"+/content\" to require authentication at and below \"/content\" and" +
" \"-/system/sling/login\" to not require authentication at and below" +
" \"/system/sling/login\". By default this list is empty. This list is extended at" +
" run time with additional entries: One entry is added for the \"Allow Anonymous" +
" Access\" configuration. Other entries are added for any services setting the" +
" \"sling.auth.requirements\" service registration property.")
String[] sling_auth_requirements(); // NOSONAR
@AttributeDefinition(name = "Anonymous User Name",
description = "Defines which user name to assume" +
" for anonymous requests, that is requests not providing credentials" +
" supported by any of the registered authentication handlers. If this" +
" property is missing or empty, the default is assumed which depends on" +
" the resource provider(s). Otherwise anonymous requests are handled with" +
" this user name. If the configured user name does not exist or is not" +
" allowed to access the resource data, anonymous requests may still be" +
" blocked. If anonymous access is not allowed, this property is ignored.")
String sling_auth_anonymous_user(); // NOSONAR
@AttributeDefinition(name = "Anonymous User Password",
description = "Password for the anonymous" +
" user defined in the Anonymous User Name field. This property is only" +
" used if a non-empty anonymous user name is configured. If this property" +
" is not defined but a password is required, an empty password would be" +
" assumed.", type = AttributeType.PASSWORD)
String sling_auth_anonymous_password(); // NOSONAR
@AttributeDefinition(name = "HTTP Basic Authentication",
description = "Level of support for HTTP Basic Authentication. Such" +
" support can be provided in three levels: (1) no support at all, that is" +
" disabled, (2) preemptive support, that is HTTP Basic Authentication is" +
" supported if the authentication header is set in the request, (3) full" +
" support. The default is preemptive support unless Anonymous Access is" +
" not allowed. In this case HTTP Basic Authentication is always enabled" +
" to ensure clients can authenticate at least with basic authentication.",
options = {
@Option(label = "Enabled", value = HTTP_AUTH_ENABLED),
@Option(label = "Enabled (Preemptive)", value = HTTP_AUTH_PREEMPTIVE),
@Option(label = "Disabled", value = HTTP_AUTH_DISABLED)
})
String auth_http() default HTTP_AUTH_PREEMPTIVE; // NOSONAR
@AttributeDefinition(name = "Realm",
description = "HTTP BASIC authentication realm. This property" +
" is only used if the HTTP Basic Authentication support is not disabled. The" +
" default value is \"Sling (Development)\".")
String auth_http_realm() default "Sling (Development)"; // NOSONAR
@AttributeDefinition(name = "Authentication URI Suffices",
description = "A list of request URI suffixes intended to" +
" be handled by Authentication Handlers. Any request whose request URI" +
" ends with any one of the listed suffices is intended to be handled by" +
" an Authentication Handler causing the request to either be rejected or" +
" the client being redirected to another location and thus the request not" +
" being further processed after the authentication phase. The default is" +
" just \"/j_security_check\" which is the suffix defined by the Servlet API" +
" specification used for FORM based authentication.")
String[] auth_uri_suffix() default DEFAULT_AUTH_URI_SUFFIX; // NOSONAR
}
/** default logger */
private final Logger log = LoggerFactory.getLogger(SlingAuthenticator.class);
/**
* Value of the {@link Config#auth_http()} property to fully enable the built-in
* HTTP Authentication Handler (value is "enabled").
*/
static final String HTTP_AUTH_ENABLED = "enabled";
/**
* Value of the {@link Config#auth_http()} property to completely disable the
* built-in HTTP Authentication Handler (value is "disabled").
*/
static final String HTTP_AUTH_DISABLED = "disabled";
/**
* Value of the {@link Config#auth_http()} property to enable extracting the
* credentials if the HTTP Basic authentication header is present (value is
* "preemptive"). In <i>preemptive</i> mode, though, the
* <code>requestCredentials</code> and <code>dropCredentials</code> methods
* will not send back a 401 response.
*/
static final String HTTP_AUTH_PREEMPTIVE = "preemptive";
/**
* Default request URI suffix to expect to be handled by authentication
* handlers and not expecting to cause
* {@link #handleSecurity(HttpServletRequest, HttpServletResponse)} to
* return <code>true</code>.
*/
static final String DEFAULT_AUTH_URI_SUFFIX = "/j_security_check";
/**
* The name of the form submission parameter providing the new password of
* the user (value is "j_newpassword").
*/
private static final String PAR_NEW_PASSWORD = "j_newpassword";
/**
* The name of the {@link AuthenticationInfo} property providing the option
* {@link org.apache.sling.auth.core.spi.AuthenticationFeedbackHandler}
* handler to be called back on login failure or success.
*/
private static final String AUTH_INFO_PROP_FEEDBACK_HANDLER = "$$sling.auth.AuthenticationFeedbackHandler$$";
/** The name of the impersonation parameter */
private volatile String sudoParameterName;
/** The name of the impersonation cookie */
private volatile String sudoCookieName;
/**
* The configured URI suffices indicating a authentication requests and
* requiring redirects and thus returning <code>false</code> from the
* #handleSecurity method.
* <p>
* This will be <code>null</code> if there are no suffices to consider.
*/
private volatile String[] authUriSuffices; // NOSONAR
/**
* The name of the user to assume for anonymous access. By default this is
* <code>null</code> to use <code>null</code> credentials and thus use the
* system provided identification.
*
* @see #getAnonymousCredentials()
*/
private volatile String anonUser;
/**
* The password to use for anonymous access. This property is only used if
* the {@link #anonUser} field is not <code>null</code>.
*
* @see #getAnonymousCredentials()
*/
private volatile char[] anonPassword; // NOSONAR
/** HTTP Basic authentication handler */
private volatile HttpBasicAuthenticationHandler httpBasicHandler; // NOSONAR
/**
* The manager for services registered with "sling.auth.requirements" to
* update the internal authentication requirements
*/
private final PathBasedHolderCache<AuthenticationRequirementHolder> authenticationRequirementsManager;
/**
* Manager for authentication handlers.
*/
private final PathBasedHolderCache<AbstractAuthenticationHandlerHolder> authHandlersManager;
/**
* AuthenticationInfoPostProcessor services
*/
@Reference(cardinality = ReferenceCardinality.MULTIPLE, service = AuthenticationInfoPostProcessor.class, fieldOption = FieldOption.REPLACE)
private volatile List<AuthenticationInfoPostProcessor> authInfoPostProcessors = Collections.emptyList(); // NOSONAR
/**
* The event admin service.
*/
@Reference(policy=ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
private volatile EventAdmin eventAdmin; // NOSONAR
@Reference(policy=ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
private volatile SlingAuthenticationMetrics metricsService;
private final ResourceResolverFactory resourceResolverFactory;
/**
* LoginEventDecorator services
*/
@Reference(cardinality = ReferenceCardinality.MULTIPLE, service = LoginEventDecorator.class, fieldOption = FieldOption.REPLACE)
private volatile List<LoginEventDecorator> loginEventDecorators = new ArrayList<>(); // NOSONAR
// ---------- SCR integration
@Activate
public SlingAuthenticator(@Reference AuthenticationRequirementsManager authReqManager,
@Reference AuthenticationHandlersManager authHandlerManager,
@Reference(policyOption = ReferencePolicyOption.GREEDY) final ResourceResolverFactory resourceResolverFactory,
final BundleContext bundleContext,
final Config config) {
this.resourceResolverFactory = resourceResolverFactory;
this.authenticationRequirementsManager = authReqManager;
this.authHandlersManager = authHandlerManager;
this.modified(config);
}
@Modified
private void modified(final Config config) {
if (!config.auth_sudo_cookie().equals(this.sudoCookieName)) {
log.info(
"modified: Setting new cookie name for impersonation {} (was {})",
config.auth_sudo_cookie(), this.sudoCookieName);
this.sudoCookieName = config.auth_sudo_cookie();
}
if (!config.auth_sudo_parameter().equals(this.sudoParameterName)) {
log.info(
"modified: Setting new parameter name for impersonation {} (was {})",
config.auth_sudo_parameter(), this.sudoParameterName);
this.sudoParameterName = config.auth_sudo_parameter();
}
if (config.sling_auth_anonymous_user() != null && config.sling_auth_anonymous_user().length() > 0) {
this.anonUser = config.sling_auth_anonymous_user();
this.anonPassword = config.sling_auth_anonymous_password() == null ? "".toCharArray() : config.sling_auth_anonymous_password().toCharArray();
} else {
this.anonUser = null;
this.anonPassword = null;
}
this.authUriSuffices = config.auth_uri_suffix();
if (!config.auth_annonymous()) {
log.info("modified: Anonymous Access is denied thus HTTP Basic Authentication is fully enabled");
}
final String http = getHttpAuth(config);
if (HTTP_AUTH_DISABLED.equals(http)) {
this.httpBasicHandler = null;
} else {
this.httpBasicHandler = new HttpBasicAuthenticationHandler(config.auth_http_realm(), HTTP_AUTH_ENABLED.equals(http));
}
}
/**
* Get the configuration for the http auth
* @param config The configuration
* @return The http auth
*/
public static String getHttpAuth(final Config config) {
final String http;
if (config.auth_annonymous()) {
http = config.auth_http();
} else {
http = HTTP_AUTH_ENABLED;
}
return http;
}
// --------- AuthenticationSupport interface
/**
* Checks the authentication contained in the request. This check is only
* based on the original request object, no URI translation has taken place
* yet.
* <p>
*
* @param request The request object containing the information for the
* authentication.
* @param response The response object which may be used to send the
* information on the request failure to the user.
* @return <code>true</code> if request processing should continue assuming
* successful authentication. If <code>false</code> is returned it
* is assumed a response has been sent to the client and the request
* is terminated.
*/
@Override
public boolean handleSecurity(HttpServletRequest request,
HttpServletResponse response) {
// 0. Nothing to do, if the session is also in the request
// this might be the case if the request is handled as a result
// of a servlet container include inside another Sling request
Object sessionAttr = request.getAttribute(REQUEST_ATTRIBUTE_RESOLVER);
if (sessionAttr instanceof ResourceResolver) {
log.debug("handleSecurity: Request already authenticated, nothing to do");
return true;
} else if (sessionAttr != null) {
// warn and remove existing non-session
log.warn("handleSecurity: Overwriting existing ResourceResolver attribute ({})", sessionAttr);
request.removeAttribute(REQUEST_ATTRIBUTE_RESOLVER);
}
boolean process = false;
final SlingAuthenticationMetrics local = this.metricsService;
final Closeable ctx = local != null ? local.authenticationTimerContext() : null;
try {
process = doHandleSecurity(request, response);
if (process && expectAuthenticationHandler(request)) {
log.warn("handleSecurity: AuthenticationHandler did not block request; access denied");
request.removeAttribute(AuthenticationHandler.FAILURE_REASON);
request.removeAttribute(AuthenticationHandler.FAILURE_REASON_CODE);
AuthUtil.sendInvalid(request, response);
process = false;
}
} finally {
if (local != null) {
try {
ctx.close();
} catch (final IOException e) {
// ignore
}
local.authenticateCompleted(process);
}
}
return process;
}
private boolean doHandleSecurity(HttpServletRequest request, HttpServletResponse response) {
// 0. Check for request attribute; set if not present
Object authUriSufficesAttr = request
.getAttribute(AuthConstants.ATTR_REQUEST_AUTH_URI_SUFFIX);
if (authUriSufficesAttr == null && authUriSuffices != null) {
request.setAttribute(AuthConstants.ATTR_REQUEST_AUTH_URI_SUFFIX,
authUriSuffices);
}
// 1. Ask all authentication handlers to try to extract credentials
final AuthenticationInfo authInfo = getAuthenticationInfo(request, response);
// 2. PostProcess credentials
try {
postProcess(authInfo, request, response);
} catch (LoginException e) {
postLoginFailedEvent(request, authInfo, e);
handleLoginFailure(request, response, authInfo, e);
return false;
}
// 3. Check Credentials
if (authInfo == AuthenticationInfo.DOING_AUTH) {
log.debug("doHandleSecurity: ongoing authentication in the handler");
return false;
} else if (authInfo == AuthenticationInfo.FAIL_AUTH) {
log.debug("doHandleSecurity: Credentials present but not valid, request authentication again");
AuthUtil.setLoginResourceAttribute(request, request.getRequestURI());
doLogin(request, response);
return false;
} else if (authInfo.getAuthType() == null) {
log.debug("doHandleSecurity: No credentials in the request, anonymous");
return getAnonymousResolver(request, response, authInfo);
} else {
log.debug("doHandleSecurity: Trying to get a session for {}", authInfo.getUser());
return getResolver(request, response, authInfo);
}
}
// ---------- Authenticator interface
/**
* Requests authentication information from the client. Returns
* <code>true</code> if the information has been requested and request
* processing can be terminated. Otherwise the request information could not
* be requested and the request should be terminated with a 403/FORBIDDEN
* response.
* <p>
* Any response sent by the handler is also handled by the error handler
* infrastructure.
*
* @param request The request object
* @param response The response object to which to send the request
* @throws IllegalStateException If response is already committed
* @throws NoAuthenticationHandlerException If no authentication handler
* claims responsibility to authenticate the request.
*/
@Override
public void login(HttpServletRequest request, HttpServletResponse response) {
// ensure the response is not committed yet
if (response.isCommitted()) {
throw new IllegalStateException("Response already committed");
}
// select path used for authentication handler selection
final Collection<AbstractAuthenticationHandlerHolder>[] holdersArray = this.authHandlersManager
.findApplicableHolders(request);
final String path = getHandlerSelectionPath(request);
boolean done = false;
for (int m = 0; !done && m < holdersArray.length; m++) {
final Collection<AbstractAuthenticationHandlerHolder> holderList = holdersArray[m];
if ( holderList != null ) {
for (AbstractAuthenticationHandlerHolder holder : holderList) {
if (holder.isPathRequiresHandler(path)) {
log.debug("login: requesting authentication using handler: {}",
holder);
try {
done = holder.requestCredentials(request, response);
} catch (IOException ioe) {
log.error(
"login: Failed sending authentication request through handler "
+ holder + ", access forbidden", ioe);
done = true;
}
if (done) {
break;
}
}
}
}
}
// fall back to HTTP Basic handler (if not done already)
if (!done && httpBasicHandler != null) {
done = httpBasicHandler.requestCredentials(request, response);
}
// no handler could send an authentication request, throw
if (!done) {
int size = 0;
for (int m = 0; m < holdersArray.length; m++) {
if (holdersArray[m] != null) {
size += holdersArray[m].size();
}
}
log.info("login: No handler for request ({} handlers available)", size);
throw new NoAuthenticationHandlerException();
}
}
/**
* Logs out the user calling all applicable
* {@link org.apache.sling.auth.core.spi.AuthenticationHandler}
* authentication handlers.
*/
@Override
public void logout(HttpServletRequest request, HttpServletResponse response) {
// ensure the response is not committed yet
if (response.isCommitted()) {
throw new IllegalStateException("Response already committed");
}
// make sure impersonation is dropped
setSudoCookie(request, response, new AuthenticationInfo("dummy", request.getRemoteUser()));
final String path = getHandlerSelectionPath(request);
final Collection<AbstractAuthenticationHandlerHolder>[] holdersArray = this.authHandlersManager
.findApplicableHolders(request);
for (int m = 0; m < holdersArray.length; m++) {
final Collection<AbstractAuthenticationHandlerHolder> holderSet = holdersArray[m];
if (holderSet != null) {
for (AbstractAuthenticationHandlerHolder holder : holderSet) {
if (holder.isPathRequiresHandler(path)) {
log.debug("logout: dropping authentication using handler: {}",
holder);
try {
holder.dropCredentials(request, response);
} catch (IOException ioe) {
log.error(
"logout: Failed dropping authentication through handler "
+ holder, ioe);
}
}
}
}
}
if (httpBasicHandler != null) {
httpBasicHandler.dropCredentials(request, response);
}
redirectAfterLogout(request, response);
}
// ---------- ServletRequestListener
@Override
public void requestInitialized(ServletRequestEvent sre) {
// don't care
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
ServletRequest request = sre.getServletRequest();
Object resolverAttr = request.getAttribute(REQUEST_ATTRIBUTE_RESOLVER);
if (resolverAttr instanceof ResourceResolver) {
((ResourceResolver) resolverAttr).close();
request.removeAttribute(REQUEST_ATTRIBUTE_RESOLVER);
}
}
// ---------- internal
/**
* Get the request path from the request
* @param request The request
* @return The path
*/
private String getPath(final HttpServletRequest request) {
final StringBuilder sb = new StringBuilder();
if (request.getServletPath() != null) {
sb.append(request.getServletPath());
}
if (request.getPathInfo() != null) {
sb.append(request.getPathInfo());
}
String path = sb.toString();
// Get the path used to select the authenticator, if the SlingServlet
// itself has been requested without any more info, this will be empty
// and we assume the root (SLING-722)
if (path.length() == 0) {
path = "/";
}
return path;
}
private AuthenticationInfo getAuthenticationInfo(HttpServletRequest request, HttpServletResponse response) {
String path = getPath(request);
final Collection<AbstractAuthenticationHandlerHolder>[] localArray = this.authHandlersManager
.findApplicableHolders(request);
for (int m = 0; m < localArray.length; m++) {
final Collection<AbstractAuthenticationHandlerHolder> local = localArray[m];
if (local != null) {
for (AbstractAuthenticationHandlerHolder holder : local) {
if (holder.isPathRequiresHandler(path)){
final AuthenticationInfo authInfo = holder.extractCredentials(
request, response);
if (authInfo != null) {
// skip the put call for known read-only objects
if (authInfo != AuthenticationInfo.DOING_AUTH &&
authInfo != AuthenticationInfo.FAIL_AUTH) {
// add the feedback handler to the info (may be null)
authInfo.put(AUTH_INFO_PROP_FEEDBACK_HANDLER,
holder.getFeedbackHandler());
}
return authInfo;
}
}
}
}
}
// check whether the HTTP Basic handler can extract the header
if (httpBasicHandler != null) {
final AuthenticationInfo authInfo = httpBasicHandler.extractCredentials(
request, response);
if (authInfo != null) {
authInfo.put(AUTH_INFO_PROP_FEEDBACK_HANDLER, httpBasicHandler);
return authInfo;
}
}
// no handler found for the request ....
log.debug("getAuthenticationInfo: no handler could extract credentials; assuming anonymous");
return getAnonymousCredentials();
}
/**
* Run through the available post processors.
*/
private void postProcess(AuthenticationInfo info, HttpServletRequest request, HttpServletResponse response)
throws LoginException {
final List<AuthenticationInfoPostProcessor> localList = this.authInfoPostProcessors;
for (final AuthenticationInfoPostProcessor processor : localList) {
processor.postProcess(info, request, response);
}
}
/**
* Try to acquire a ResourceResolver as indicated by authInfo
*
* @return <code>true</code> if request processing should continue assuming
* successful authentication. If <code>false</code> is returned it
* is assumed a response has been sent to the client and the request
* is terminated.
*/
private boolean getResolver(final HttpServletRequest request,
final HttpServletResponse response,
final AuthenticationInfo authInfo) {
// prepare the feedback handler
final AuthenticationFeedbackHandler feedbackHandler = (AuthenticationFeedbackHandler) authInfo.remove(AUTH_INFO_PROP_FEEDBACK_HANDLER);
final Object sendLoginEvent = authInfo.remove(AuthConstants.AUTH_INFO_LOGIN);
// try to connect
try {
handleImpersonation(request, authInfo);
handlePasswordChange(request, authInfo);
ResourceResolver resolver = resourceResolverFactory.getResourceResolver(authInfo);
final boolean impersChanged = setSudoCookie(request, response, authInfo);
if (sendLoginEvent != null) {
postLoginEvent(request, authInfo);
}
// provide the resource resolver to the feedback handler
request.setAttribute(REQUEST_ATTRIBUTE_RESOLVER, resolver);
boolean processRequest = true;
// custom feedback handler with option to redirect
if (feedbackHandler != null) {
processRequest = !feedbackHandler.authenticationSucceeded(request, response, authInfo);
}
if (processRequest) {
if (AuthUtil.isValidateRequest(request)) {
AuthUtil.sendValid(response);
processRequest = false;
} else if (impersChanged || feedbackHandler == null) {
processRequest = !DefaultAuthenticationFeedbackHandler.handleRedirect(request, response);
}
}
if (processRequest) {
// process: set required attributes
setAttributes(resolver, authInfo.getAuthType(), request);
} else {
// terminate: cleanup
resolver.close();
}
return processRequest;
} catch (LoginException re) {
postLoginFailedEvent(request, authInfo, re);
// handle failure feedback before proceeding to handling the
// failed login internally
if (feedbackHandler != null) {
feedbackHandler.authenticationFailed(request, response,
authInfo);
}
// now find a way to get credentials unless the feedback handler
// has committed a response to the client already
if (!response.isCommitted()) {
return handleLoginFailure(request, response, authInfo, re);
}
}
// end request
return false;
}
private boolean expectAuthenticationHandler(final HttpServletRequest request) {
if (this.authUriSuffices != null) {
final String requestUri = request.getRequestURI();
for (final String uriSuffix : this.authUriSuffices) {
if (requestUri.endsWith(uriSuffix)) {
return true;
}
}
}
return false;
}
/** Try to acquire an anonymous ResourceResolver */
private boolean getAnonymousResolver(final HttpServletRequest request,
final HttpServletResponse response, final AuthenticationInfo authInfo) {
// Get an anonymous session if allowed, or if we are handling
// a request for the login servlet
if (isAnonAllowed(request)) {
try {
ResourceResolver resolver = resourceResolverFactory.getResourceResolver(authInfo);
// check whether the client asked for redirect after
// authentication and/or impersonation
if (DefaultAuthenticationFeedbackHandler.handleRedirect(
request, response)) {
// request will now be terminated, so close the resolver
// to release resources
resolver.close();
return false;
}
// set the attributes for further processing
setAttributes(resolver, null, request);
return true;
} catch (LoginException re) {
// cannot login > fail login, do not try to authenticate
handleLoginFailure(request, response, new AuthenticationInfo(null, "anonymous user"), re);
return false;
}
}
// If we get here, anonymous access is not allowed: redirect
// to the login servlet
log.debug("getAnonymousResolver: Anonymous access not allowed by configuration - requesting credentials");
doLogin(request, response);
// fallback to no session
return false;
}
boolean isAnonAllowed(HttpServletRequest request) {
String path = getPath(request);
final Collection<AuthenticationRequirementHolder>[] holderSetArray = this.authenticationRequirementsManager.findApplicableHolders(request);
for (int m = 0; m < holderSetArray.length; m++) {
final Collection<AuthenticationRequirementHolder> holders = holderSetArray[m];
if (holders != null) {
for (AuthenticationRequirementHolder holder : holders) {
if (holder.isPathRequiresHandler(path)) {
return !holder.requiresAuthentication();
}
}
}
}
// fallback to anonymous not allowed (aka authentication required)
return false;
}
/**
* Returns credentials to use for anonymous resource access. If an anonymous
* user is configued, this returns an {@link AuthenticationInfo} instance
* whose authentication type is <code>null</code> and the user name and
* password are set according to the {@link #PAR_ANONYMOUS_USER} and
* {@link #PAR_ANONYMOUS_PASSWORD} configurations. Otherwise
* the user name and password fields are just <code>null</code>.
*/
private AuthenticationInfo getAnonymousCredentials() {
AuthenticationInfo info = new AuthenticationInfo(null);
if (this.anonUser != null) {
info.setUser(this.anonUser);
info.setPassword(this.anonPassword);
}
return info;
}
private boolean handleLoginFailure(final HttpServletRequest request,
final HttpServletResponse response, final AuthenticationInfo authInfo,
final Exception reason) {
String user = authInfo.getUser();
boolean processRequest = false;
if (reason.getClass().getName().contains("TooManySessionsException")) {
// to many users, send a 503 Service Unavailable
log.info("handleLoginFailure: Too many sessions for {}: {}", user,
reason.getMessage());
try {
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
"SlingAuthenticator: Too Many Users");
} catch (IOException ioe) {
log.error(
"handleLoginFailure: Cannot send status 503 to client", ioe);
}
} else if (reason instanceof LoginException) {
log.info("handleLoginFailure: Unable to authenticate {}: {}", user,
reason.getMessage());
if (isAnonAllowed(request) && !expectAuthenticationHandler(request) && !AuthUtil.isValidateRequest(request)) {
log.debug("handleLoginFailure: LoginException on an anonymous resource, fallback to getAnonymousResolver");
processRequest = getAnonymousResolver(request, response, new AuthenticationInfo(null));
} else {
// request authentication information and send 403 (Forbidden)
// if no handler can request authentication information.
FAILURE_REASON_CODES code = FailureCodesMapper.getFailureReason(authInfo, reason);
String message = null;
switch (code) {
case ACCOUNT_LOCKED:
message = "Account is locked";
break;
case ACCOUNT_NOT_FOUND:
message = "Account was not found";
break;
case PASSWORD_EXPIRED:
message = "Password expired";
break;
case PASSWORD_EXPIRED_AND_NEW_PASSWORD_IN_HISTORY:
message = "Password expired and new password found in password history";
break;
case EXPIRED_TOKEN:
message = "Expired authentication token";
break;
case UNKNOWN:
case INVALID_LOGIN:
default:
message = "User name and password do not match";
break;
}
// preset a reason for the login failure
request.setAttribute(AuthenticationHandler.FAILURE_REASON_CODE, code);
ensureAttribute(request, AuthenticationHandler.FAILURE_REASON, message);
doLogin(request, response);
}
} else {
if (log.isErrorEnabled()) {
// general problem, send a 500 Internal Server Error
log.error(String.format("handleLoginFailure: Unable to authenticate %s", user),
reason);
}
try {
response.sendError(
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"SlingAuthenticator: data access error, reason="
+ reason.getClass().getSimpleName());
} catch (IOException ioe) {
log.error(
"handleLoginFailure: Cannot send status 500 to client", ioe);
}
}
return processRequest;
}
/**
* Tries to request credentials from the client. The following mechanisms
* are implemented by this method:
* <ul>
* <li>If the request is a credentials validation request (see
* {@link AuthUtil#isValidateRequest(HttpServletRequest)}
* ) a 403/FORBIDDEN response is sent back.</li>
* <li>If the request is not considered a
* {@link #isBrowserRequest(HttpServletRequest) browser request} and the
* HTTP Basic Authentication Handler is at least enabled for preemptive
* credentials processing, a 401/UNAUTHORIZED response is sent back. This
* helps implementing HTTP Basic authentication with WebDAV clients. If HTTP
* Basic Authentication is completely switched of a 403/FORBIDDEN response
* is sent back instead.</li>
* <li>If the request is considered an
* {@link #isAjaxRequest(HttpServletRequest) Ajax request} a 403/FORBIDDEN
* response is simply sent back because we assume an Ajax requestor cannot
* properly handle any request for credentials graciously.</li>
* <li>Otherwise the {@link #login(HttpServletRequest, HttpServletResponse)}
* method is called to try to find and call an authentication handler to
* request credentials from the client. If none is available or willing to
* request credentials, a 403/FORBIDDEN response is also sent back to the
* client.</li>
* </ul>
* <p>
* If a 403/FORBIDDEN response is sent back the {@link AuthUtil#X_REASON} header is
* set to a either the value of the
* {@link AuthenticationHandler#FAILURE_REASON} request attribute or to some
* generic description describing the reason. To actually send the response
* the
* {@link AuthUtil#sendInvalid(HttpServletRequest, HttpServletResponse)}
* method is called.
* <p>
* This method is called in three situations:
* <ul>
* <li>If the request contains no credentials but anonymous login is not
* allowed</li>
* <li>If the request contains credentials but getting the Resource Resolver
* using the provided credentials fails</li>
* <li>If the selected authentication handler indicated any presented
* credentials are not valid</li>
* </ul>
*
* @param request The current request
* @param response The response to send the credentials request (or access
* denial to)
* @see AuthUtil#isValidateRequest(HttpServletRequest)
* @see #isBrowserRequest(HttpServletRequest)
* @see #isAjaxRequest(HttpServletRequest)
* @see AuthUtil#sendInvalid(HttpServletRequest,
* HttpServletResponse)
*/
private void doLogin(HttpServletRequest request,
HttpServletResponse response) {
if (!AuthUtil.isValidateRequest(request)) {
if (AuthUtil.isBrowserRequest(request)) {
if (!AuthUtil.isAjaxRequest(request) && !isLoginLoop(request)) {
try {
login(request, response);
return;
} catch (IllegalStateException ise) {
log.error("doLogin: Cannot login: Response already committed");
return;
} catch (NoAuthenticationHandlerException nahe) {
/*
* Don't set the failureReason for missing
* authentication handlers to not disclose this setup
* information.
*/
log.error("doLogin: Cannot login: No AuthenticationHandler available to handle the request");
}
}
} else {
// Presumably this is WebDAV. If HTTP Basic is fully enabled or
// enabled for preemptive credential support, we just request
// HTTP Basic credentials. Otherwise (HTTP Basic is fully
// switched off, 403 is sent back)
if (httpBasicHandler != null) {
httpBasicHandler.sendUnauthorized(response);
return;
}
}
}
// if we are here, we cannot redirect to the login form because it is
// an XHR request or because there is no authentication handler willing
// request credentials from the client or because it is a failed
// credential validation
// ensure a failure reason
ensureAttribute(request, AuthenticationHandler.FAILURE_REASON,
"Authentication Failed");
AuthUtil.sendInvalid(request, response);
}
private boolean isExpiredToken(HttpServletRequest request) {
return FAILURE_REASON_CODES.EXPIRED_TOKEN == request.getAttribute(AuthenticationHandler.FAILURE_REASON_CODE);
}
/**
* Returns <code>true</code> if the current request was referred to by the
* same URL as the current request has. This is assumed to be caused by a
* loop in requesting credentials from the client. Such a loop will probably
* never cause the request for credentials to succeed, so it must be broken.
*
* @param request The request to check
* @return <code>true</code> if the request is considered to be a loop;
* <code>false</code> otherwise
*/
private boolean isLoginLoop(final HttpServletRequest request) {
if (isExpiredToken(request))
return false;
String referer = request.getHeader("Referer");
if (referer != null) {
StringBuffer sb = request.getRequestURL();
if (request.getQueryString() != null) {
sb.append('?').append(request.getQueryString());
}
return referer.equals(sb.toString());
}
// no referer means no loop
return false;
}
/**
* Sets the name request attribute to the given value unless the request
* attribute is already set a non-<code>null</code> value.
*
* @param request The request on which to set the attribute
* @param attribute The name of the attribute to check/set
* @param value The value to set the attribute to if it is not already set
*/
private void ensureAttribute(final HttpServletRequest request,
final String attribute, final Object value) {
if (request.getAttribute(attribute) == null) {
request.setAttribute(attribute, value);
}
}
/**
* Sets the request attributes required by the OSGi HttpContext interface
* specification for the <code>handleSecurity</code> method. In addition the
* {@link SlingAuthenticator#REQUEST_ATTRIBUTE_RESOLVER} request attribute
* is set to the ResourceResolver.
*/
private void setAttributes(final ResourceResolver resolver, final String authType,
final HttpServletRequest request) {
// HttpService API required attributes
request.setAttribute(ServletContextHelper.AUTHENTICATION_TYPE, authType);
if ( authType != null ) {
request.setAttribute(ServletContextHelper.REMOTE_USER, resolver.getUserID());
}
// resource resolver for down-stream use
request.setAttribute(REQUEST_ATTRIBUTE_RESOLVER, resolver);
log.debug(
"setAttributes: ResourceResolver stored as request attribute: user={}",
resolver.getUserID());
}
/**
* Sends the session cookie for the name session with the given age in
* seconds. This sends a Version 1 cookie.
*
* @param request The {@link HttpServletRequest}
* @param response The {@link HttpServletResponse} on which to send
* back the cookie.
* @param user The name of the user to impersonate as. This will be quoted
* and used as the cookie value.
* @param maxAge The maximum age of the cookie in seconds. Positive values
* are persisted on the browser for the indicated number of
* seconds, setting the age to 0 (zero) causes the cookie to be
* deleted in the browser and using a negative value defines a
* temporary cookie to be deleted when the browser exits.
* @param path The cookie path to use. This is intended to be the web
* application's context path. If this is empty or
* <code>null</code> the root path will be used assuming the web
* application is registered at the root context.
* @param owner The name of the user originally authenticated in the request
* and who is now impersonating as <i>user</i>.
*/
private void sendSudoCookie(
HttpServletRequest request,
HttpServletResponse response,
final String user, final int maxAge, final String path,
final String owner) {
final String quotedUser;
String quotedOwner = null;
try {
quotedUser = quoteCookieValue(user);
if (owner != null) {
quotedOwner = quoteCookieValue(owner);
}
} catch (IllegalArgumentException iae) {
log.error(
"sendSudoCookie: Failed to quote value '{}' of cookie {}: {}",
user, this.sudoCookieName, iae.getMessage());
return;
} catch (UnsupportedEncodingException e) {
log.error(
"sendSudoCookie: Failed to quote value '{}' of cookie {}: {}",
user, this.sudoCookieName, e.getMessage());
return;
}
if (quotedUser != null) {
final Cookie cookie = new Cookie(this.sudoCookieName, quotedUser);
cookie.setHttpOnly(true);
cookie.setSecure(request.isSecure());
cookie.setMaxAge(maxAge);
cookie.setPath((path == null || path.length() == 0) ? "/" : path);
try {
cookie.setComment(quotedOwner + " impersonates as " +quotedUser);
} catch (IllegalArgumentException iae) {
// ignore
}
response.addCookie(cookie);
}
}
/**
* Handles impersonation based on the request parameter for impersonation
* (see {@link #sudoParameterName}) and the current setting in the sudo
* cookie.
* <p>
* If the sudo parameter is empty or missing, the current cookie setting for
* impersonation is used. Else if the parameter is <code>-</code>, the
* current cookie impersonation is removed and no impersonation will take
* place for this request. Else the parameter is assumed to contain the name
* of a user to impersonate as.
*
* @param req The {@link HttpServletRequest} optionally containing
* the sudo parameter.
* @param authInfo The authentication info into which the
* <code>sudo.user.id</code> property is set to the impersonator
* user.
*/
private void handleImpersonation(HttpServletRequest req,
AuthenticationInfo authInfo) {
String currentSudo = getSudoCookieValue(req);
/**
* sudo parameter : empty or missing to continue to use the setting
* already stored in the session; or "-" to remove impersonation
* altogether (also from the session); or the handle of a user page to
* impersonate as that user (if possible)
*/
String sudo = req.getParameter(this.sudoParameterName);
if (sudo == null || sudo.length() == 0) {
sudo = currentSudo;
} else if ("-".equals(sudo)) {
sudo = null;
}
// sudo the session if needed
if (sudo != null && sudo.length() > 0) {
authInfo.put(ResourceResolverFactory.USER_IMPERSONATION, sudo);
}
}
/**
* Handles password change based on the request parameter for the new password
* (see {@link #newPasswordParameterName}).
* <p>
* If the new password request parameter is present, it is added to the authInfo
* object, which is later transformed to SimpleCredentials attributes.
*
* @param req The {@link HttpServletRequest} optionally containing
* the new password parameter.
* @param authInfo The authentication info into which the
* <code>newPassword</code> property is set.
*/
private void handlePasswordChange(HttpServletRequest req, AuthenticationInfo authInfo) {
String newPassword = req.getParameter(PAR_NEW_PASSWORD );
if (newPassword != null && newPassword.length() > 0) {
authInfo.put("user.newpassword", newPassword);
}
}
private String getSudoCookieValue(HttpServletRequest req) {
// the current state of impersonation
String currentSudo = null;
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (int i = 0; currentSudo == null && i < cookies.length; i++) {
if (sudoCookieName.equals(cookies[i].getName())) {
currentSudo = unquoteCookieValue(cookies[i].getValue());
}
}
}
return currentSudo;
}
/**
* Sets the impersonation cookie on the response if impersonation actually
* changed and returns whether the cookie has been set (or cleared) or not.
* <p>
* The current impersonation state is taken from the sudo cookie value
* while the desired state is taken from the user.impersonation
* property of the auth info. If they don't match, the sudo cookie
* is set according to the user.impersonation property where the
* cookie is actually cleared if the property is <code>null</code>.
*
* @param req Providing the current sudo cookie value
* @param res For setting the sudo cookie
* @param authInfo Providing information about desired impersonation
* @return <code>true</code> if the cookie has been set or cleared or
* <code>false</code> if the cookie is not modified.
*/
boolean setSudoCookie(HttpServletRequest req,
HttpServletResponse res, AuthenticationInfo authInfo) {
final String sudo = (String) authInfo.get(ResourceResolverFactory.USER_IMPERSONATION);
final String currentSudo = getSudoCookieValue(req);
boolean changed = false;
// set the (new) impersonation
if (sudo == null && currentSudo != null) {
// Parameter set to "-" to clear impersonation, which was
// active due to cookie setting
// clear impersonation
this.sendSudoCookie(req, res, "", 0, req.getContextPath(), authInfo.getUser());
changed = true;
} else if (sudo != null && !sudo.equals(currentSudo)) {
// Parameter set to a name. As the cookie is not set yet
// or is set to another name, send the cookie with current sudo
// (re-)set impersonation
this.sendSudoCookie(req, res, sudo, -1, req.getContextPath(), sudo);
changed = true;
}
return changed;
}
/**
* Returns the path to be used to select the authentication handler to login
* or logout with.
* <p>
* This method uses the {@link Authenticator#LOGIN_RESOURCE} request
* attribute. If this attribute is not set (or is not a string), the request
* path info is used. If this is not set either, or is the empty string, "/"
* is returned.
*
* @param request The request providing the request attribute or path info.
* @return The path as set by the request attribute or the path info or "/"
* if neither is set.
*/
private String getHandlerSelectionPath(HttpServletRequest request) {
final Object loginPathO = request.getAttribute(Authenticator.LOGIN_RESOURCE);
String path;
if (loginPathO instanceof String) {
path = (String) loginPathO;
final String ctxPath = request.getContextPath();
if (ctxPath != null && path.startsWith(ctxPath)) {
path = path.substring(ctxPath.length());
}
if (path == null || path.length() == 0) {
path = "/";
}
} else {
path = getPath(request);
}
return path;
}
/**
* If the response has not been committed yet, redirect to target requested
* by the <code>resource</code> request attribute or parameter. If neither
* is set to a non-null string, the request is redirected to the context
* root.
* <p>
* The response is not reset though, since the handler may have set states
* such as an updated HTTP session or some Cookie
*
* @param request The request providing the redirect target
* @param response The response to send the redirect to
*/
private void redirectAfterLogout(final HttpServletRequest request,
final HttpServletResponse response) {
// nothing more to do if the response has already been committed
if (response.isCommitted()) {
log.debug("redirectAfterLogout: Response has already been committed, not redirecting");
return;
}
// find the redirect target from the resource attribute or parameter
// falling back to the request context path (or /) if not set or invalid
String target = AuthUtil.getLoginResource(request, request.getContextPath());
if (!AuthUtil.isRedirectValid(request, target)) {
log.warn("redirectAfterLogout: Desired redirect target is invalid; redirecting to '/'");
target = request.getContextPath() + "/";
}
// redirect to there
try {
response.sendRedirect(target); // NOSONAR The redirect is checked for validity using AuthUtil
} catch (IOException e) {
log.error("Failed to redirect to the page: " + target, e);
}
}
private void postLoginEvent(final HttpServletRequest request, final AuthenticationInfo authInfo) {
final Map<String, Object> properties = new HashMap<>();
properties.put(SlingConstants.PROPERTY_USERID, authInfo.getUser());
properties.put(AuthenticationInfo.AUTH_TYPE, authInfo.getAuthType());
// allow extensions to supply additional properties
final List<LoginEventDecorator> localList = this.loginEventDecorators;
for (final LoginEventDecorator decorator : localList) {
decorator.decorateLoginEvent(request, authInfo, properties);
}
EventAdmin localEA = this.eventAdmin;
if (localEA != null) {
localEA.postEvent(new Event(AuthConstants.TOPIC_LOGIN, properties));
}
}
/**
* Post an event to let subscribers know that a login failure has occurred. For examples, subscribers
* to the {@link AuthConstants#TOPIC_LOGIN_FAILED} event topic may be used to implement a failed login throttling solution.
*/
private void postLoginFailedEvent(final HttpServletRequest request,
final AuthenticationInfo authInfo, Exception reason) {
// The reason for the failure may be useful to downstream subscribers.
FAILURE_REASON_CODES reasonCode = FailureCodesMapper.getFailureReason(authInfo, reason);
//if reason code is unknowm, it is problem some non-login related failure, so don't send the event
if (reasonCode != FAILURE_REASON_CODES.UNKNOWN) {
final Map<String, Object> properties = new HashMap<>();
if (authInfo.getUser() != null) {
properties.put(SlingConstants.PROPERTY_USERID, authInfo.getUser());
}
if (authInfo.getAuthType() != null) {
properties.put(AuthenticationInfo.AUTH_TYPE, authInfo.getAuthType());
}
properties.put("reason_code", reasonCode.name());
// allow extensions to supply additional properties
final List<LoginEventDecorator> localList = this.loginEventDecorators;
for (final LoginEventDecorator decorator : localList) {
decorator.decorateLoginFailedEvent(request, authInfo, properties);
}
EventAdmin localEA = this.eventAdmin;
if (localEA != null) {
localEA.postEvent(new Event(AuthConstants.TOPIC_LOGIN_FAILED, properties));
}
}
}
/**
* Ensures the cookie value is properly quoted for transmission to the
* client.
* <p>
* The problem is, that the character set of cookie values is limited by
* RFC 2109 defining that a cookie value must be token or quoted-string
* according to RFC-2616:
* <pre>
* token = 1*<any CHAR except CTLs or separators>
* separators = "(" | ")" | "<" | ">" | "@"
* | "," | ";" | ":" | "\" | <">
* | "/" | "[" | "]" | "?" | "="
* | "{" | "}" | SP | HT
*
* quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
* qdtext = <any TEXT except <">>
* quoted-pair = "\" CHAR
*
* @param value The cookie value to quote
* @return The quoted cookie value
* @throws UnsupportedEncodingException
* @throws IllegalArgumentException If the cookie value is <code>null</code>
* or cannot be quoted, primarily because it contains a quote
* sign.
*/
static String quoteCookieValue(final String value) throws UnsupportedEncodingException {
// method is package private to enable unit testing
if (value == null) {
throw new IllegalArgumentException("Cookie value may not be null");
}
StringBuilder builder = new StringBuilder(value.length() * 2);
builder.append('"');
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (c == '"') {
builder.append("\\\"");
} else if (c == '@') {
builder.append(c);
} else if (c == 127 || (c < 32 && c != '\t')) {
throw new IllegalArgumentException(
"Cookie value may not contain CTL character");
} else {
builder.append(URLEncoder.encode(String.valueOf(c), "UTF-8"));
}
}
builder.append('"');
return builder.toString();
}
/**
* Removes (optional) quotes from a cookie value to get the raw value of the
* cookie.
*
* @param value The cookie value to unquote
* @return The unquoted cookie value
*/
static String unquoteCookieValue(String value) {
// method is package private to enable unit testing
// return value unmodified if null or empty
if (value == null || value.length() == 0) {
return value;
}
if (value.startsWith("\"") && value.endsWith("\"")) {
value = value.substring(1, value.length()-1);
}
StringBuilder builder = new StringBuilder();
String [] values = value.split("\\\\");
for (String v:values) {
try {
builder.append(URLDecoder.decode(v, "UTF-8"));
} catch (UnsupportedEncodingException e) {
builder.append(v);
}
}
return builder.toString();
}
}