FormAuthenticator.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.catalina.authenticator;

  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.security.Principal;
  21. import java.util.Enumeration;
  22. import java.util.Iterator;
  23. import java.util.Locale;

  24. import javax.servlet.RequestDispatcher;
  25. import javax.servlet.http.Cookie;
  26. import javax.servlet.http.HttpServletRequest;
  27. import javax.servlet.http.HttpServletResponse;
  28. import javax.servlet.http.HttpSession;

  29. import org.apache.catalina.Globals;
  30. import org.apache.catalina.Realm;
  31. import org.apache.catalina.Session;
  32. import org.apache.catalina.connector.Request;
  33. import org.apache.catalina.connector.Response;
  34. import org.apache.coyote.ActionCode;
  35. import org.apache.coyote.ContinueResponseTiming;
  36. import org.apache.juli.logging.Log;
  37. import org.apache.juli.logging.LogFactory;
  38. import org.apache.tomcat.util.ExceptionUtils;
  39. import org.apache.tomcat.util.buf.ByteChunk;
  40. import org.apache.tomcat.util.buf.MessageBytes;
  41. import org.apache.tomcat.util.descriptor.web.LoginConfig;
  42. import org.apache.tomcat.util.http.MimeHeaders;

  43. /**
  44.  * An <b>Authenticator</b> and <b>Valve</b> implementation of FORM BASED Authentication, as described in the Servlet API
  45.  * Specification.
  46.  *
  47.  * @author Craig R. McClanahan
  48.  * @author Remy Maucherat
  49.  */
  50. public class FormAuthenticator extends AuthenticatorBase {

  51.     private final Log log = LogFactory.getLog(FormAuthenticator.class); // must not be static


  52.     // ----------------------------------------------------- Instance Variables

  53.     /**
  54.      * Character encoding to use to read the username and password parameters from the request. If not set, the encoding
  55.      * of the request body will be used.
  56.      */
  57.     protected String characterEncoding = null;

  58.     /**
  59.      * Landing page to use if a user tries to access the login page directly or if the session times out during login.
  60.      * If not set, error responses will be sent instead.
  61.      */
  62.     protected String landingPage = null;

  63.     /**
  64.      * If the authentication process creates a session, this is the maximum session timeout (in seconds) during the
  65.      * authentication process. Once authentication is complete, the default session timeout will apply. Sessions that
  66.      * exist before the authentication process starts will retain their original session timeout throughout.
  67.      */
  68.     protected int authenticationSessionTimeout = 120;


  69.     // ------------------------------------------------------------- Properties

  70.     /**
  71.      * Return the character encoding to use to read the user name and password.
  72.      *
  73.      * @return The name of the character encoding
  74.      */
  75.     public String getCharacterEncoding() {
  76.         return characterEncoding;
  77.     }


  78.     /**
  79.      * Set the character encoding to be used to read the user name and password.
  80.      *
  81.      * @param encoding The name of the encoding to use
  82.      */
  83.     public void setCharacterEncoding(String encoding) {
  84.         characterEncoding = encoding;
  85.     }


  86.     /**
  87.      * Return the landing page to use when FORM auth is mis-used.
  88.      *
  89.      * @return The path to the landing page relative to the web application root
  90.      */
  91.     public String getLandingPage() {
  92.         return landingPage;
  93.     }


  94.     /**
  95.      * Set the landing page to use when the FORM auth is mis-used.
  96.      *
  97.      * @param landingPage The path to the landing page relative to the web application root
  98.      */
  99.     public void setLandingPage(String landingPage) {
  100.         this.landingPage = landingPage;
  101.     }


  102.     /**
  103.      * Returns the maximum session timeout to be used during authentication if the authentication process creates a
  104.      * session.
  105.      *
  106.      * @return the maximum session timeout to be used during authentication if the authentication process creates a
  107.      *             session
  108.      */
  109.     public int getAuthenticationSessionTimeout() {
  110.         return authenticationSessionTimeout;
  111.     }


  112.     /**
  113.      * Configures the maximum session timeout to be used during authentication if the authentication process creates a
  114.      * session.
  115.      *
  116.      * @param authenticationSessionTimeout The maximum session timeout to use duriing authentication if the
  117.      *                                         authentication process creates a session
  118.      */
  119.     public void setAuthenticationSessionTimeout(int authenticationSessionTimeout) {
  120.         this.authenticationSessionTimeout = authenticationSessionTimeout;
  121.     }


  122.     // ------------------------------------------------------ Protected Methods

  123.     /**
  124.      * Authenticate the user making this request, based on the specified login configuration. Return <code>true</code>
  125.      * if any specified constraint has been satisfied, or <code>false</code> if we have created a response challenge
  126.      * already.
  127.      *
  128.      * @param request  Request we are processing
  129.      * @param response Response we are creating
  130.      *
  131.      * @exception IOException if an input/output error occurs
  132.      */
  133.     @Override
  134.     protected boolean doAuthenticate(Request request, HttpServletResponse response) throws IOException {

  135.         // References to objects we will need later
  136.         Session session = null;
  137.         Principal principal = null;

  138.         // Have we authenticated this user before but have caching disabled?
  139.         if (!cache) {
  140.             session = request.getSessionInternal(true);
  141.             if (log.isTraceEnabled()) {
  142.                 log.trace("Checking for reauthenticate in session " + session);
  143.             }
  144.             String username = (String) session.getNote(Constants.SESS_USERNAME_NOTE);
  145.             String password = (String) session.getNote(Constants.SESS_PASSWORD_NOTE);
  146.             if (username != null && password != null) {
  147.                 if (log.isTraceEnabled()) {
  148.                     log.trace("Reauthenticating username '" + username + "'");
  149.                 }
  150.                 principal = context.getRealm().authenticate(username, password);
  151.                 if (principal != null) {
  152.                     register(request, response, principal, HttpServletRequest.FORM_AUTH, username, password);
  153.                     if (!matchRequest(request)) {
  154.                         return true;
  155.                     }
  156.                 }
  157.                 if (log.isDebugEnabled()) {
  158.                     log.debug(sm.getString("formAuthenticator.reauthFailed"));
  159.                 }
  160.             }
  161.         }

  162.         // Is this the re-submit of the original request URI after successful
  163.         // authentication? If so, forward the *original* request instead.
  164.         if (matchRequest(request)) {
  165.             session = request.getSessionInternal(true);
  166.             if (log.isTraceEnabled()) {
  167.                 log.trace("Restore request from session '" + session.getIdInternal() + "'");
  168.             }
  169.             if (restoreRequest(request, session)) {
  170.                 if (log.isTraceEnabled()) {
  171.                     log.trace("Proceed to restored request");
  172.                 }
  173.                 return true;
  174.             } else {
  175.                 if (log.isDebugEnabled()) {
  176.                     log.debug(sm.getString("formAuthenticator.restoreFailed"));
  177.                 }
  178.                 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
  179.                 return false;
  180.             }
  181.         }

  182.         // This check has to be after the previous check for a matching request
  183.         // because that matching request may also include a cached Principal.
  184.         if (checkForCachedAuthentication(request, response, true)) {
  185.             return true;
  186.         }

  187.         // Acquire references to objects we will need to evaluate
  188.         String contextPath = request.getContextPath();
  189.         String requestURI = request.getDecodedRequestURI();

  190.         // Is this the action request from the login page?
  191.         boolean loginAction = requestURI.startsWith(contextPath) && requestURI.endsWith(Constants.FORM_ACTION);

  192.         LoginConfig config = context.getLoginConfig();

  193.         // No -- Save this request and redirect to the form login page
  194.         if (!loginAction) {
  195.             // If this request was to the root of the context without a trailing
  196.             // '/', need to redirect to add it else the submit of the login form
  197.             // may not go to the correct web application
  198.             if (request.getServletPath().length() == 0 && request.getPathInfo() == null) {
  199.                 StringBuilder location = new StringBuilder(requestURI);
  200.                 location.append('/');
  201.                 if (request.getQueryString() != null) {
  202.                     location.append('?');
  203.                     location.append(request.getQueryString());
  204.                 }
  205.                 response.sendRedirect(response.encodeRedirectURL(location.toString()));
  206.                 return false;
  207.             }

  208.             session = request.getSessionInternal(true);
  209.             if (log.isTraceEnabled()) {
  210.                 log.trace("Save request in session '" + session.getIdInternal() + "'");
  211.             }
  212.             try {
  213.                 saveRequest(request, session);
  214.             } catch (IOException ioe) {
  215.                 log.debug(sm.getString("authenticator.requestBodyTooBig"));
  216.                 response.sendError(HttpServletResponse.SC_FORBIDDEN, sm.getString("authenticator.requestBodyTooBig"));
  217.                 return false;
  218.             }
  219.             forwardToLoginPage(request, response, config);
  220.             return false;
  221.         }

  222.         // Yes -- Acknowledge the request, validate the specified credentials
  223.         // and redirect to the error page if they are not correct
  224.         request.getResponse().sendAcknowledgement(ContinueResponseTiming.ALWAYS);
  225.         Realm realm = context.getRealm();
  226.         if (characterEncoding != null) {
  227.             request.setCharacterEncoding(characterEncoding);
  228.         }
  229.         String username = request.getParameter(Constants.FORM_USERNAME);
  230.         String password = request.getParameter(Constants.FORM_PASSWORD);
  231.         if (log.isTraceEnabled()) {
  232.             log.trace("Authenticating username '" + username + "'");
  233.         }
  234.         principal = realm.authenticate(username, password);
  235.         if (principal == null) {
  236.             forwardToErrorPage(request, response, config);
  237.             return false;
  238.         }

  239.         if (log.isTraceEnabled()) {
  240.             log.trace("Authentication of '" + username + "' was successful");
  241.         }

  242.         if (session == null) {
  243.             session = request.getSessionInternal(false);
  244.         }
  245.         if (session != null && getChangeSessionIdOnAuthentication()) {
  246.             // Does session id match?
  247.             String expectedSessionId = (String) session.getNote(Constants.SESSION_ID_NOTE);
  248.             if (expectedSessionId == null || !expectedSessionId.equals(request.getRequestedSessionId())) {
  249.                 if (log.isDebugEnabled()) {
  250.                     log.debug(sm.getString("formAuthenticator.sessionIdMismatch", session.getId(), expectedSessionId));
  251.                 }
  252.                 session.expire();
  253.                 session = null;
  254.             }
  255.         }
  256.         if (session == null) {
  257.             if (containerLog.isDebugEnabled()) {
  258.                 containerLog.debug(sm.getString("formAuthenticator.sessionExpired"));
  259.             }
  260.             if (landingPage == null) {
  261.                 response.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT,
  262.                         sm.getString("authenticator.sessionExpired"));
  263.             } else {
  264.                 // Make the authenticator think the user originally requested
  265.                 // the landing page
  266.                 String uri = request.getContextPath() + landingPage;
  267.                 SavedRequest saved = new SavedRequest();
  268.                 saved.setMethod("GET");
  269.                 saved.setRequestURI(uri);
  270.                 saved.setDecodedRequestURI(uri);
  271.                 request.getSessionInternal(true).setNote(Constants.FORM_REQUEST_NOTE, saved);
  272.                 response.sendRedirect(response.encodeRedirectURL(uri));
  273.             }
  274.             return false;
  275.         }

  276.         register(request, response, principal, HttpServletRequest.FORM_AUTH, username, password);

  277.         // Redirect the user to the original request URI (which will cause
  278.         // the original request to be restored)
  279.         requestURI = savedRequestURL(session);
  280.         if (log.isTraceEnabled()) {
  281.             log.trace("Redirecting to original '" + requestURI + "'");
  282.         }
  283.         if (requestURI == null) {
  284.             if (landingPage == null) {
  285.                 response.sendError(HttpServletResponse.SC_BAD_REQUEST, sm.getString("authenticator.formlogin"));
  286.             } else {
  287.                 // Make the authenticator think the user originally requested
  288.                 // the landing page
  289.                 String uri = request.getContextPath() + landingPage;
  290.                 SavedRequest saved = new SavedRequest();
  291.                 saved.setMethod("GET");
  292.                 saved.setRequestURI(uri);
  293.                 saved.setDecodedRequestURI(uri);
  294.                 session.setNote(Constants.FORM_REQUEST_NOTE, saved);
  295.                 response.sendRedirect(response.encodeRedirectURL(uri));
  296.             }
  297.         } else {
  298.             // Until the Servlet API allows specifying the type of redirect to
  299.             // use.
  300.             Response internalResponse = request.getResponse();
  301.             String location = response.encodeRedirectURL(requestURI);
  302.             if ("HTTP/1.1".equals(request.getProtocol())) {
  303.                 internalResponse.sendRedirect(location, HttpServletResponse.SC_SEE_OTHER);
  304.             } else {
  305.                 internalResponse.sendRedirect(location, HttpServletResponse.SC_FOUND);
  306.             }
  307.         }
  308.         return false;
  309.     }


  310.     @Override
  311.     protected boolean isContinuationRequired(Request request) {
  312.         // Special handling for form-based logins to deal with the case
  313.         // where the login form (and therefore the "j_security_check" URI
  314.         // to which it submits) might be outside the secured area
  315.         String contextPath = this.context.getPath();
  316.         String decodedRequestURI = request.getDecodedRequestURI();
  317.         if (decodedRequestURI.startsWith(contextPath) && decodedRequestURI.endsWith(Constants.FORM_ACTION)) {
  318.             return true;
  319.         }

  320.         // Special handling for form-based logins to deal with the case where
  321.         // a resource is protected for some HTTP methods but not protected for
  322.         // GET which is used after authentication when redirecting to the
  323.         // protected resource.
  324.         // TODO: This is similar to the FormAuthenticator.matchRequest() logic
  325.         // Is there a way to remove the duplication?
  326.         Session session = request.getSessionInternal(false);
  327.         if (session != null) {
  328.             SavedRequest savedRequest = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE);
  329.             if (savedRequest != null && decodedRequestURI.equals(savedRequest.getDecodedRequestURI())) {
  330.                 return true;
  331.             }
  332.         }

  333.         return false;
  334.     }


  335.     @Override
  336.     protected String getAuthMethod() {
  337.         return HttpServletRequest.FORM_AUTH;
  338.     }


  339.     @Override
  340.     protected void register(Request request, HttpServletResponse response, Principal principal, String authType,
  341.             String username, String password, boolean alwaysUseSession, boolean cache) {

  342.         super.register(request, response, principal, authType, username, password, alwaysUseSession, cache);

  343.         // If caching an authenticated Principal is turned off,
  344.         // store username and password as session notes to use them for re-authentication.
  345.         if (!cache) {
  346.             Session session = request.getSessionInternal(false);
  347.             if (session != null) {
  348.                 if (username != null) {
  349.                     session.setNote(Constants.SESS_USERNAME_NOTE, username);
  350.                 } else {
  351.                     session.removeNote(Constants.SESS_USERNAME_NOTE);
  352.                 }
  353.                 if (password != null) {
  354.                     session.setNote(Constants.SESS_PASSWORD_NOTE, password);
  355.                 } else {
  356.                     session.removeNote(Constants.SESS_PASSWORD_NOTE);
  357.                 }
  358.             }
  359.         }
  360.     }


  361.     /**
  362.      * Called to forward to the login page
  363.      *
  364.      * @param request  Request we are processing
  365.      * @param response Response we are populating
  366.      * @param config   Login configuration describing how authentication should be performed
  367.      *
  368.      * @throws IOException If the forward to the login page fails and the call to
  369.      *                         {@link HttpServletResponse#sendError(int, String)} throws an {@link IOException}
  370.      */
  371.     protected void forwardToLoginPage(Request request, HttpServletResponse response, LoginConfig config)
  372.             throws IOException {

  373.         if (log.isDebugEnabled()) {
  374.             log.debug(sm.getString("formAuthenticator.forwardLogin", request.getRequestURI(), request.getMethod(),
  375.                     config.getLoginPage(), context.getName()));
  376.         }

  377.         String loginPage = config.getLoginPage();
  378.         if (loginPage == null || loginPage.length() == 0) {
  379.             String msg = sm.getString("formAuthenticator.noLoginPage", context.getName());
  380.             log.warn(msg);
  381.             response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg);
  382.             return;
  383.         }

  384.         if (getChangeSessionIdOnAuthentication()) {
  385.             Session session = request.getSessionInternal(false);
  386.             if (session != null) {
  387.                 String oldSessionId = session.getId();
  388.                 String newSessionId = changeSessionID(request, session);
  389.                 session.setNote(Constants.SESSION_ID_NOTE, newSessionId);
  390.                 if (log.isDebugEnabled()) {
  391.                     log.debug(sm.getString("formAuthenticator.changeSessionIdLogin", oldSessionId, newSessionId));
  392.                 }
  393.             }
  394.         }

  395.         // Always use GET for the login page, regardless of the method used
  396.         String oldMethod = request.getMethod();
  397.         request.getCoyoteRequest().method().setString("GET");

  398.         RequestDispatcher disp = context.getServletContext().getRequestDispatcher(loginPage);
  399.         try {
  400.             if (context.fireRequestInitEvent(request.getRequest())) {
  401.                 disp.forward(request.getRequest(), response);
  402.                 context.fireRequestDestroyEvent(request.getRequest());
  403.             }
  404.         } catch (Throwable t) {
  405.             ExceptionUtils.handleThrowable(t);
  406.             String msg = sm.getString("formAuthenticator.forwardLoginFail");
  407.             log.warn(msg, t);
  408.             request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
  409.             response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg);
  410.         } finally {
  411.             // Restore original method so that it is written into access log
  412.             request.getCoyoteRequest().method().setString(oldMethod);
  413.         }
  414.     }


  415.     /**
  416.      * Called to forward to the error page
  417.      *
  418.      * @param request  Request we are processing
  419.      * @param response Response we are populating
  420.      * @param config   Login configuration describing how authentication should be performed
  421.      *
  422.      * @throws IOException If the forward to the error page fails and the call to
  423.      *                         {@link HttpServletResponse#sendError(int, String)} throws an {@link IOException}
  424.      */
  425.     protected void forwardToErrorPage(Request request, HttpServletResponse response, LoginConfig config)
  426.             throws IOException {

  427.         String errorPage = config.getErrorPage();
  428.         if (errorPage == null || errorPage.length() == 0) {
  429.             String msg = sm.getString("formAuthenticator.noErrorPage", context.getName());
  430.             log.warn(msg);
  431.             response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg);
  432.             return;
  433.         }

  434.         RequestDispatcher disp = context.getServletContext().getRequestDispatcher(config.getErrorPage());
  435.         try {
  436.             if (context.fireRequestInitEvent(request.getRequest())) {
  437.                 disp.forward(request.getRequest(), response);
  438.                 context.fireRequestDestroyEvent(request.getRequest());
  439.             }
  440.         } catch (Throwable t) {
  441.             ExceptionUtils.handleThrowable(t);
  442.             String msg = sm.getString("formAuthenticator.forwardErrorFail");
  443.             log.warn(msg, t);
  444.             request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
  445.             response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg);
  446.         }
  447.     }


  448.     /**
  449.      * Does this request match the saved one (so that it must be the redirect we signaled after successful
  450.      * authentication?
  451.      *
  452.      * @param request The request to be verified
  453.      *
  454.      * @return <code>true</code> if the requests matched the saved one
  455.      */
  456.     protected boolean matchRequest(Request request) {
  457.         // Has a session been created?
  458.         Session session = request.getSessionInternal(false);
  459.         if (session == null) {
  460.             return false;
  461.         }

  462.         // Is there a saved request?
  463.         SavedRequest sreq = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE);
  464.         if (sreq == null) {
  465.             return false;
  466.         }

  467.         // Is there a saved principal?
  468.         if (cache && session.getPrincipal() == null || !cache && request.getPrincipal() == null) {
  469.             return false;
  470.         }

  471.         // Does session id match?
  472.         if (getChangeSessionIdOnAuthentication()) {
  473.             String expectedSessionId = (String) session.getNote(Constants.SESSION_ID_NOTE);
  474.             if (expectedSessionId == null || !expectedSessionId.equals(request.getRequestedSessionId())) {
  475.                 return false;
  476.             }
  477.         }

  478.         // Does the request URI match?
  479.         String decodedRequestURI = request.getDecodedRequestURI();
  480.         if (decodedRequestURI == null) {
  481.             return false;
  482.         }
  483.         return decodedRequestURI.equals(sreq.getDecodedRequestURI());
  484.     }


  485.     /**
  486.      * Restore the original request from information stored in our session. If the original request is no longer present
  487.      * (because the session timed out), return <code>false</code>; otherwise, return <code>true</code>.
  488.      *
  489.      * @param request The request to be restored
  490.      * @param session The session containing the saved information
  491.      *
  492.      * @return <code>true</code> if the request was successfully restored
  493.      *
  494.      * @throws IOException if an IO error occurred during the process
  495.      */
  496.     protected boolean restoreRequest(Request request, Session session) throws IOException {

  497.         // Retrieve and remove the SavedRequest object from our session
  498.         SavedRequest saved = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE);
  499.         session.removeNote(Constants.FORM_REQUEST_NOTE);
  500.         session.removeNote(Constants.SESSION_ID_NOTE);
  501.         if (saved == null) {
  502.             return false;
  503.         }

  504.         // Swallow any request body since we will be replacing it
  505.         // Need to do this before headers are restored as AJP connector uses
  506.         // content length header to determine how much data needs to be read for
  507.         // request body
  508.         byte[] buffer = new byte[4096];
  509.         InputStream is = request.createInputStream();
  510.         while (is.read(buffer) >= 0) {
  511.             // Ignore request body
  512.         }

  513.         // Modify our current request to reflect the original one
  514.         request.clearCookies();
  515.         Iterator<Cookie> cookies = saved.getCookies();
  516.         while (cookies.hasNext()) {
  517.             request.addCookie(cookies.next());
  518.         }

  519.         String method = saved.getMethod();
  520.         MimeHeaders rmh = request.getCoyoteRequest().getMimeHeaders();
  521.         rmh.recycle();
  522.         boolean cacheable = "GET".equalsIgnoreCase(method) || "HEAD".equalsIgnoreCase(method);
  523.         Iterator<String> names = saved.getHeaderNames();
  524.         while (names.hasNext()) {
  525.             String name = names.next();
  526.             // The browser isn't expecting this conditional response now.
  527.             // Assuming that it can quietly recover from an unexpected 412.
  528.             // BZ 43687
  529.             if (!("If-Modified-Since".equalsIgnoreCase(name) ||
  530.                     (cacheable && "If-None-Match".equalsIgnoreCase(name)))) {
  531.                 Iterator<String> values = saved.getHeaderValues(name);
  532.                 while (values.hasNext()) {
  533.                     rmh.addValue(name).setString(values.next());
  534.                 }
  535.             }
  536.         }

  537.         request.clearLocales();
  538.         Iterator<Locale> locales = saved.getLocales();
  539.         while (locales.hasNext()) {
  540.             request.addLocale(locales.next());
  541.         }

  542.         request.getCoyoteRequest().getParameters().recycle();

  543.         ByteChunk body = saved.getBody();

  544.         if (body != null) {
  545.             request.getCoyoteRequest().action(ActionCode.REQ_SET_BODY_REPLAY, body);

  546.             // Set content type
  547.             MessageBytes contentType = MessageBytes.newInstance();

  548.             // If no content type specified, use default for POST
  549.             String savedContentType = saved.getContentType();
  550.             if (savedContentType == null && "POST".equalsIgnoreCase(method)) {
  551.                 savedContentType = Globals.CONTENT_TYPE_FORM_URL_ENCODING;
  552.             }

  553.             contentType.setString(savedContentType);
  554.             request.getCoyoteRequest().setContentType(contentType);
  555.         }

  556.         request.getCoyoteRequest().method().setString(method);
  557.         // The method, URI, queryString and protocol are normally stored as
  558.         // bytes in the HttpInputBuffer and converted lazily to String. At this
  559.         // point, the method has already been set as String in the line above
  560.         // but the URI, queryString and protocol are still in byte form in the
  561.         // HttpInputBuffer. Processing the saved request body will overwrite
  562.         // these bytes. Configuring the HttpInputBuffer to retain these bytes as
  563.         // it would in a normal request would require some invasive API changes.
  564.         // Therefore force the conversion to String now so the correct values
  565.         // are presented if the application requests them.
  566.         request.getCoyoteRequest().requestURI().toStringType();
  567.         request.getCoyoteRequest().queryString().toStringType();
  568.         request.getCoyoteRequest().protocol().toStringType();

  569.         if (saved.getOriginalMaxInactiveInterval() > 0) {
  570.             session.setMaxInactiveInterval(saved.getOriginalMaxInactiveInterval());
  571.         }

  572.         return true;
  573.     }


  574.     /**
  575.      * Save the original request information into our session.
  576.      *
  577.      * @param request The request to be saved
  578.      * @param session The session to contain the saved information
  579.      *
  580.      * @throws IOException if an IO error occurred during the process
  581.      */
  582.     protected void saveRequest(Request request, Session session) throws IOException {

  583.         // Create and populate a SavedRequest object for this request
  584.         SavedRequest saved = new SavedRequest();
  585.         Cookie cookies[] = request.getCookies();
  586.         if (cookies != null) {
  587.             for (Cookie cookie : cookies) {
  588.                 saved.addCookie(cookie);
  589.             }
  590.         }
  591.         Enumeration<String> names = request.getHeaderNames();
  592.         while (names.hasMoreElements()) {
  593.             String name = names.nextElement();
  594.             Enumeration<String> values = request.getHeaders(name);
  595.             while (values.hasMoreElements()) {
  596.                 String value = values.nextElement();
  597.                 saved.addHeader(name, value);
  598.             }
  599.         }
  600.         Enumeration<Locale> locales = request.getLocales();
  601.         while (locales.hasMoreElements()) {
  602.             Locale locale = locales.nextElement();
  603.             saved.addLocale(locale);
  604.         }

  605.         // May need to acknowledge a 100-continue expectation
  606.         request.getResponse().sendAcknowledgement(ContinueResponseTiming.ALWAYS);

  607.         int maxSavePostSize = request.getConnector().getMaxSavePostSize();
  608.         if (maxSavePostSize != 0) {
  609.             ByteChunk body = new ByteChunk();
  610.             body.setLimit(maxSavePostSize);

  611.             byte[] buffer = new byte[4096];
  612.             int bytesRead;
  613.             InputStream is = request.getInputStream();

  614.             while ((bytesRead = is.read(buffer)) >= 0) {
  615.                 body.append(buffer, 0, bytesRead);
  616.             }

  617.             // Only save the request body if there is something to save
  618.             if (body.getLength() > 0) {
  619.                 saved.setContentType(request.getContentType());
  620.                 saved.setBody(body);
  621.             }
  622.         }

  623.         saved.setMethod(request.getMethod());
  624.         saved.setQueryString(request.getQueryString());
  625.         saved.setRequestURI(request.getRequestURI());
  626.         saved.setDecodedRequestURI(request.getDecodedRequestURI());

  627.         SavedRequest previousSavedRequest = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE);
  628.         if (session instanceof HttpSession) {
  629.             if (((HttpSession) session).isNew()) {
  630.                 int originalMaxInactiveInterval = session.getMaxInactiveInterval();
  631.                 if (originalMaxInactiveInterval > getAuthenticationSessionTimeout()) {
  632.                     saved.setOriginalMaxInactiveInterval(originalMaxInactiveInterval);
  633.                     session.setMaxInactiveInterval(getAuthenticationSessionTimeout());
  634.                 }
  635.             } else if (previousSavedRequest != null && previousSavedRequest.getOriginalMaxInactiveInterval() > 0) {
  636.                 /*
  637.                  * The user may have refreshed the browser page during authentication. Transfer the original max
  638.                  * inactive interval from previous saved request to current one else, once authentication is completed,
  639.                  * the session will retain the the shorter authentication session timeout
  640.                  */
  641.                 saved.setOriginalMaxInactiveInterval(previousSavedRequest.getOriginalMaxInactiveInterval());
  642.             }
  643.         }

  644.         // Stash the SavedRequest in our session for later use
  645.         session.setNote(Constants.FORM_REQUEST_NOTE, saved);
  646.     }


  647.     /**
  648.      * Return the request URI (with the corresponding query string, if any) from the saved request so that we can
  649.      * redirect to it.
  650.      *
  651.      * @param session Our current session
  652.      *
  653.      * @return the original request URL
  654.      */
  655.     protected String savedRequestURL(Session session) {
  656.         SavedRequest saved = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE);
  657.         if (saved == null) {
  658.             return null;
  659.         }
  660.         StringBuilder sb = new StringBuilder(saved.getRequestURI());
  661.         if (saved.getQueryString() != null) {
  662.             sb.append('?');
  663.             sb.append(saved.getQueryString());
  664.         }

  665.         // Avoid protocol relative redirects
  666.         while (sb.length() > 1 && sb.charAt(1) == '/') {
  667.             sb.deleteCharAt(0);
  668.         }

  669.         return sb.toString();
  670.     }
  671. }