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(); } }