package org.apereo.cas.authentication.principal;
import org.apereo.cas.util.EncodingUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Encapsulates a Response to send back for a particular service.
*
* @author Scott Battaglia
* @author Arnaud Lesueur
* @since 3.1
*/
public class DefaultResponse implements Response {
/**
* Log instance.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultResponse.class);
/**
* Pattern to detect unprintable ASCII characters.
*/
private static final Pattern NON_PRINTABLE = Pattern.compile("[\\x00-\\x1F\\x7F]+");
private static final int CONST_REDIRECT_RESPONSE_MULTIPLIER = 40;
private static final int CONST_REDIRECT_RESPONSE_BUFFER = 100;
private static final long serialVersionUID = -8251042088720603062L;
private ResponseType responseType;
private String url;
private Map<String, String> attributes;
/**
* Instantiates a new response.
*
* @param responseType the response type
* @param url the url
* @param attributes the attributes
*/
protected DefaultResponse(final ResponseType responseType, final String url, final Map<String, String> attributes) {
this.responseType = responseType;
this.url = url;
this.attributes = attributes;
}
/**
* Gets the post response.
*
* @param url the url
* @param attributes the attributes
* @return the post response
*/
public static Response getPostResponse(final String url, final Map<String, String> attributes) {
return new DefaultResponse(ResponseType.POST, url, attributes);
}
/**
* Gets the redirect response.
*
* @param url the url
* @param parameters the parameters
* @return the redirect response
*/
public static Response getRedirectResponse(final String url, final Map<String, String> parameters) {
final StringBuilder builder = new StringBuilder(parameters.size()
* CONST_REDIRECT_RESPONSE_MULTIPLIER + CONST_REDIRECT_RESPONSE_BUFFER);
final String sanitizedUrl = sanitizeUrl(url);
LOGGER.debug("Sanitized URL for redirect response is [{}]", sanitizedUrl);
final String[] fragmentSplit = sanitizedUrl.split("#");
builder.append(fragmentSplit[0]);
final String params = parameters.entrySet()
.stream()
.filter(entry -> entry.getValue() != null).map(entry -> {
String param;
try {
param = String.join("=", entry.getKey(), EncodingUtils.urlEncode(entry.getValue()));
} catch (final RuntimeException e) {
param = String.join("=", entry.getKey(), entry.getValue());
}
return param;
})
.collect(Collectors.joining("&"));
if (!(params == null || params.isEmpty())) {
builder.append(url.contains("?") ? "&" : "?");
builder.append(params);
}
if (fragmentSplit.length > 1) {
builder.append('#');
builder.append(fragmentSplit[1]);
}
final String urlRedirect = builder.toString();
LOGGER.debug("Final redirect response is [{}]", urlRedirect);
return new DefaultResponse(ResponseType.REDIRECT, urlRedirect, parameters);
}
@Override
public Map<String, String> getAttributes() {
return this.attributes;
}
@Override
public Response.ResponseType getResponseType() {
return this.responseType;
}
@Override
public String getUrl() {
return this.url;
}
/**
* Sanitize a URL provided by a relying party by normalizing non-printable
* ASCII character sequences into spaces. This functionality protects
* against CRLF attacks and other similar attacks using invisible characters
* that could be abused to trick user agents.
*
* @param url URL to sanitize.
* @return Sanitized URL string.
*/
private static String sanitizeUrl(final String url) {
final Matcher m = NON_PRINTABLE.matcher(url);
final StringBuffer sb = new StringBuffer(url.length());
boolean hasNonPrintable = false;
while (m.find()) {
m.appendReplacement(sb, " ");
hasNonPrintable = true;
}
m.appendTail(sb);
if (hasNonPrintable) {
LOGGER.warn("The following redirect URL has been sanitized and may be sign of attack:\n[{}]", url);
}
return sb.toString();
}
}