| /* |
| * 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.spi; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| |
| import javax.servlet.http.HttpServlet; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.apache.sling.auth.core.AuthUtil; |
| |
| /** |
| * The <code>AbstractAuthenticationFormServlet</code> provides a basic |
| * implementation of a simple servlet to render a login form for authentication |
| * purposes. |
| */ |
| @SuppressWarnings("serial") |
| public abstract class AbstractAuthenticationFormServlet extends HttpServlet { |
| |
| /** |
| * The path to the default login form. |
| * |
| * @see #getDefaultFormPath() |
| */ |
| public static final String DEFAULT_FORM_PATH = "login.html"; |
| |
| /** |
| * The path to the custom login form. |
| * |
| * @see #getCustomFormPath() |
| */ |
| public static final String CUSTOM_FORM_PATH = "custom_login.html"; |
| |
| /** |
| * The raw form used by the {@link #getForm(HttpServletRequest)} method to |
| * fill in with per-request data. This field is set by the |
| * {@link #getRawForm()} method when first loading the form. |
| */ |
| private volatile String rawForm; |
| |
| /** |
| * Prepares and returns the login form. The response is sent as an UTF-8 |
| * encoded <code>text/html</code> page with all known cache control headers |
| * set to prevent all caching. |
| * <p> |
| * This servlet is to be called to handle the request directly, that is it |
| * expected to not be included and for the response to not be committed yet |
| * because it first resets the response. |
| * |
| * @throws IOException if an error occurs preparing or sending back the |
| * login form |
| * @throws IllegalStateException if the response has already been committed |
| * and thus response reset is not possible. |
| */ |
| @Override |
| protected void doGet(HttpServletRequest request, |
| HttpServletResponse response) throws IOException { |
| handle(request, response); |
| } |
| |
| /** |
| * Prepares and returns the login form. The response is sent as an UTF-8 |
| * encoded <code>text/html</code> page with all known cache control headers |
| * set to prevent all caching. |
| * <p> |
| * This servlet is to be called to handle the request directly, that is it |
| * expected to not be included and for the response to not be committed yet |
| * because it first resets the response. |
| * |
| * @throws IOException if an error occurs preparing or sending back the |
| * login form |
| * @throws IllegalStateException if the response has already been committed |
| * and thus response reset is not possible. |
| */ |
| @Override |
| protected void doPost(HttpServletRequest request, |
| HttpServletResponse response) throws IOException { |
| handle(request, response); |
| } |
| |
| private void handle(HttpServletRequest request, HttpServletResponse response) |
| throws IOException { |
| // reset the response first |
| response.reset(); |
| |
| // setup the response for HTML and cache prevention |
| response.setContentType("text/html"); |
| response.setCharacterEncoding("UTF-8"); |
| response.setHeader("Cache-Control", "no-cache"); |
| response.addHeader("Cache-Control", "no-store"); |
| response.setHeader("Pragma", "no-cache"); |
| response.setHeader("Expires", "0"); |
| |
| // send the form and flush |
| response.getWriter().print(getForm(request)); |
| response.flushBuffer(); |
| } |
| |
| /** |
| * Returns the form to be sent back to the client for login providing an |
| * optional informational message and the optional target to redirect to |
| * after successfully logging in. |
| * |
| * @param request The request providing parameters indicating the |
| * informational message and redirection target. |
| * @return The login form to be returned to the client |
| * @throws IOException If the login form cannot be loaded |
| */ |
| protected String getForm(final HttpServletRequest request) |
| throws IOException { |
| String form = getRawForm(); |
| |
| final String resource = cleanse(request, getResource(request)); |
| final String reason = getReason(request); |
| final String resourceContextPath = cleanse(request, getContextPath(request)); |
| final String contextPath = request.getContextPath(); |
| |
| // replace form placeholders with checked and filtered values |
| form = form.replace("${resource}", escape(resource)); |
| form = form.replace("${j_reason}", escape(reason)); |
| form = form.replace("${requestContextPath}", escape(resourceContextPath)); |
| form = form.replace("${contextPath}", escape(contextPath)); |
| |
| return form; |
| } |
| |
| /** |
| * Makes sure the given {@code target} is not pointing to some absolute |
| * location outside of the given {@code request} context. If so, the target |
| * must be ignored and an empty string is returned. |
| * <p> |
| * This method uses the |
| * {@link AuthUtil#isRedirectValid(HttpServletRequest, String)} method. |
| * |
| * @param request The {@code HttpServletRequest} to test the {@code target} |
| * against. |
| * @param target The target location (URL) to test for validity. |
| * @return The target location if not pointing outside of the current |
| * request or an empty string. |
| */ |
| private static String cleanse(final HttpServletRequest request, final String target) { |
| if (target.length() > 0 && !AuthUtil.isRedirectValid(request, target)) { |
| return ""; |
| } |
| return target; |
| } |
| |
| /** |
| * Escape the output. |
| * This method does a simple XML escaping for '<', '>' and '&' |
| * and also escapes single and double quotes. |
| * As these characters should never occur in a url this encoding should |
| * be fine. |
| */ |
| private static String escape(final String input) { |
| if (input == null) { |
| return null; |
| } |
| |
| final StringBuilder b = new StringBuilder(input.length()); |
| for(int i = 0;i < input.length(); i++) { |
| final char c = input.charAt(i); |
| if(c == '&') { |
| b.append("&"); |
| } else if (c == '<') { |
| b.append("<"); |
| } else if (c == '>') { |
| b.append(">"); |
| } else if (c == '"') { |
| b.append("%22"); |
| } else if (c == '\'') { |
| b.append("%27"); |
| } else { |
| b.append(c); |
| } |
| } |
| return b.toString(); |
| } |
| |
| /** |
| * Returns the path to the resource to which the request should be |
| * redirected after successfully completing the form or an empty string if |
| * there is no <code>resource</code> request parameter. |
| * |
| * @param request The request providing the <code>resource</code> parameter. |
| * @return The target to redirect after successfully login or an empty string |
| * if no specific target has been requested. |
| */ |
| protected String getResource(final HttpServletRequest request) { |
| return AuthUtil.getLoginResource(request, ""); |
| } |
| |
| /** |
| * Returns an informational message according to the value provided in the |
| * <code>j_reason</code> request parameter. Supported reasons are invalid |
| * credentials and session timeout. |
| * |
| * @param request The request providing the parameter |
| * @return The "translated" reason to render the login form or an empty |
| * string if there is no specific reason |
| */ |
| protected abstract String getReason(final HttpServletRequest request); |
| |
| /** |
| * Returns the context path for the authentication form request. This path |
| * is the path to the authenticated resource as returned by |
| * {@link #getResource(HttpServletRequest)} (without the optional query |
| * string which may be contained in the resource path). If {@link #getResource(HttpServletRequest)} |
| * return an empty string, the servlet context path is used. |
| * |
| * @param request The request |
| * @return The context path for the form action consisting of the resource to |
| * which the user is to authenticate. |
| */ |
| protected String getContextPath(final HttpServletRequest request) { |
| String contextPath = getResource(request); |
| if ("".equals(contextPath)) { |
| contextPath = request.getContextPath(); |
| } |
| int query = contextPath.indexOf('?'); |
| if (query > 0) { |
| contextPath = contextPath.substring(0, query); |
| } |
| |
| return removeEndingSlash(contextPath); |
| } |
| |
| private static String removeEndingSlash(String str) { |
| if(str != null && str.endsWith("/")) { |
| return str.substring(0, str.length() - 1); |
| } |
| return str; |
| } |
| |
| /** |
| * Load the raw unmodified form from the bundle (through the class loader). |
| * |
| * @return The raw form as a string |
| * @throws IOException If an error occurs reading the "file" or if the |
| * class loader cannot provide the form data. |
| */ |
| private String getRawForm() throws IOException { |
| if (rawForm == null) { |
| InputStream ins = null; |
| try { |
| // try a custom login page first. |
| ins = getClass().getResourceAsStream(getCustomFormPath()); |
| if (ins == null) { |
| // try the standard login page |
| ins = getClass().getResourceAsStream(getDefaultFormPath()); |
| } |
| |
| if (ins != null) { |
| StringBuilder builder = new StringBuilder(); |
| Reader r = new InputStreamReader(ins, "UTF-8"); |
| char[] cbuf = new char[1024]; |
| int rd = 0; |
| while ((rd = r.read(cbuf)) >= 0) { |
| builder.append(cbuf, 0, rd); |
| } |
| |
| rawForm = builder.toString(); |
| } |
| } finally { |
| if (ins != null) { |
| try { |
| ins.close(); |
| } catch (IOException ignore) { |
| } |
| } |
| } |
| |
| if (rawForm == null) { |
| throw new IOException("Failed reading form template"); |
| } |
| } |
| |
| return rawForm; |
| } |
| |
| /** |
| * Returns the path to the default login form to load through the class |
| * loader of this instance using <code>Class.getResourceAsStream</code>. |
| * <p> |
| * The default form is used intended to be included with the bundle |
| * implementing this abstract class. |
| * <p> |
| * This method returns {@link #DEFAULT_FORM_PATH} and may be overwritten by |
| * implementations. |
| */ |
| protected String getDefaultFormPath() { |
| return DEFAULT_FORM_PATH; |
| } |
| |
| /** |
| * Returns the path to the custom login form to load through the class |
| * loader of this instance using <code>Class.getResourceAsStream</code>. |
| * <p> |
| * The custom form can be supplied by a fragment attaching to the bundle |
| * implementing this abstract class. |
| * <p> |
| * This method returns {@link #CUSTOM_FORM_PATH} and may be overwritten by |
| * implementations. |
| */ |
| protected String getCustomFormPath() { |
| return CUSTOM_FORM_PATH; |
| } |
| } |