Mix protocols transparently in Web applications

Implement HTTP and HTTPS in a safe, flexible, and easily maintainable manner

1 2 3 Page 2
Page 2 of 3
String desiredScheme = ... ;  // From external source
String usingScheme = aRequest.getScheme();
String desiredPort =  "https".equals(desiredScheme)?httpsPort: httpPort;
String usingPort =  "https".equals(usingScheme)?httpsPort: httpPort;
if ( !desiredScheme.equals(usingScheme) ){
    StringBuffer url = HttpUtils.getRequestURL(aRequest);
    url.replace(0, usingScheme.length(), desiredScheme );
    // Find the port used within the URL string
    int startIndex = url.toString().indexOf(usingPort);
    // If the port could not be found in the URL string, it must have
    // been using a default port (80 or 443 for standard or SSL, respectively)
    if( startIndex == -1 ){
        // If desired port is not a default port, insert the port number
        // into the URL
        if((!("443".equals(desiredPort) && "https".equals(desiredScheme)))
                        && (!("80".equals(desiredPort) && "http".equals(desiredScheme)))){
            // Comes before the 3rd slash
            startIndex = url.toString().indexOf("/", url.toString().indexOf("/", url.toString().indexOf("/")+1)+1);
            url.insert(startIndex, ":" + desiredPort);
        }
    }else{  // Port could be found in URL string
         // If desired port is a default port, just delete the existing
         // port specification from the URL string
         if(("443".equals(desiredPort) && "https".equals(desiredScheme))
                        || ("80".equals(desiredPort) && "http".equals(desiredScheme))){
                    url.delete(startIndex-1, startIndex + usingPort.length());
         } else {  // Desired port is not a default port
             // Replace requested port with desired port number in URL string
             url.replace(startIndex, startIndex + usingPort.length(), desiredPort );
         }
    }
    aResponse.sendRedirect(aResponse.encodeRedirectURL(url.toString()));
}
return;

Other issues

We must consider additional issues in our solution's implementation to prevent it from changing the logic or flow of any Web application in which it is implemented.

Query string

The first consideration applies to GET requests and concerns any query string that might have been specified in the request URL. The string returned from the getRequestUrl() method does not include any query string. Thus, the query string will disappear unless it is specifically added to the URL before we redirect. This requires a slight modification to our algorithm.

Request-body parameters

Similar to the query string consideration is the issue of request parameters that might appear in a POST request's body. These parameters will also disappear unless we account for them when we redirect to another protocol. We will modify our algorithm to convert these request-body parameters into a query string when we redirect. If these body parameters include sensitive data, it will remain safe during transmission, as SSL also protects the query string.

Request attributes

Web applications can also store objects in the request as attributes in a Web application. These request attributes will also disappear during a redirect. We will modify our algorithm to temporarily store these attributes in the session during the redirect.

Cookie domain name

Most browsers treat requests to different ports on the same server as requests to different domains and therefore initiate a new session each time a user requests a different port. So our users can maintain a session across requests to our application's standard and SSL ports, we must properly configure a parameter on our application server. For WebLogic 6.1, we do that by setting the cookie domain name to our Web application's domain name in the weblogic.xml file, like so:

<session-descriptor>
   <session-param>
      <param-name>CookieDomain</param-name>
      <param-value>www.somedomain.com</param-value>
   </session-param>
</session-descriptor>

Multiple redirects

The final consideration relates to a problem that appears specific to versions 5 and higher of Microsoft's Internet Explorer (IE). These browsers require multiple redirects for each redirect specified by the application. If the application specifies a redirect to a different port using a different protocol, IE performs a redirect changing only the protocol. To change the port number, we need a second redirect. This requires us to add additional complexity to our algorithm.

Final algorithm

Taking all these additional considerations into account, our algorithm now:

  • Determines the protocol and port number used to request our Web resource
  • If the protocol and port number match the protocol and port number we want for this resource, it does nothing
  • Otherwise, it checks for a query string
  • If there is no query string, it checks for request-body parameters
  • If there are request-body parameters, it converts these parameters to a query string
  • If there are request attributes, it temporarily moves them to the session in a Map object
  • Finally, it redirects to the same URL, using the correct protocol, modifying the specified port number, if necessary, and adding any existing or generated query string

The code for this new algorithm, incorporated into utility classes, looks like this.

Returning a null URL string indicates to the calling servlet method (or JSP) that a redirect is unnecessary. Otherwise, our algorithm redirects the request to the returned URL in the calling servlet method (or JSP) as shown here:

boolean isSsl = ...;  // From external source
String redirectString =
          SslUtil.getRedirectString(aRequest, getServletContext(),  isSsl);
if( redirectString != null ){
    try{
        // Redirect the page to the desired URL
        aResponse.sendRedirect(aResponse.encodeRedirectURL(redirectString));
    } catch (IOException ioe){
       // Handle appropriately
    }
    return;
}

Implementation

We implement our solution by incorporating calls to our algorithm's utilities as the first operation in every servlet and JSP within a Web application. By incorporating redirect logic into every Web resource, we keep all our links between pages and servlets relative to a common root context. The redirect logic in each resource transparently routes each request to the correct protocol, so we do not need to specify the protocol in the links. Incorporating these calls as the first operation ensures that any redirect occurs before a response has committed.

JSP implementation through a custom tag

The most efficient alternative to duplicating calls to the utility methods in every JSP is to create and use a custom tag. With a custom tag, we can reuse a single set of calls to those methods for all our JSPs. We specify the preference for HTTP or HTTPS using an attribute of our custom tag element. The class defining our custom tag is shown here:

package article.ssl.tag;
import javax.servlet.jsp.tagext.TagSupport ;
import javax.servlet.jsp.JspException ;
import javax.servlet.http.* ;
import java.io.IOException;
import article.ssl.util.*;
public class PageSchemeTag extends TagSupport {
  private String secure = "false";  // Default value
  public void setSecure(String bool) { secure = bool;  }
  public String getSecure() { return secure;  }
  public int doStartTag() throws JspException {
     // Get the request and response objects
     HttpServletRequest request = (HttpServletRequest)pageContext.getRequest();
     HttpServletResponse response = (HttpServletResponse)pageContext.getResponse();
     String redirectString =
                SslUtil.getRedirectString(request,
                pageContext.getServletContext(),
                Boolean.valueOf(getSecure()).booleanValue());
     if( redirectString != null ){
        try{
            // Redirect the page to the desired URL
            response.sendRedirect(response.encodeRedirectURL(redirectString));
        }catch(Exception ioe){
            // Handle appropriately
        }
    }
    // Skip processing the tag body.
    return SKIP_BODY ;
  }
}

The tag library descriptor entry for this custom tag is:

  <tag>
    <name>pageScheme</name>
    <tagclass>article.ssl.tag.PageSchemeTag</tagclass>
    <bodycontent>empty</bodycontent>
    <attribute>
      <name>secure</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
    </attribute>
  </tag>

The custom tag's usage within a JSP is as follows:

<ssl:pageScheme secure="true" />

or:

<ssl:pageScheme secure="false" />

Servlet implementation through an application base class

To efficiently incorporate the redirect logic into every servlet in a Web application, we create an abstract servlet class to serve as the base class for all the Web application's servlets. The base class contains a method that calls the methods that perform the redirect logic. The base class also specifies abstract methods that must be defined in the servlets extending this class. The code for the servlet base class looks like this:

package article.ssl.servlet;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;
import java.io.*;
import java.text.*;
import article.ssl.util.*;
public abstract class BaseAppServlet extends HttpServlet{
   public void doPost(HttpServletRequest aRequest, HttpServletResponse aResponse) throws IOException, ServletException
   {
      processRequest(aRequest, aResponse);
   }
   public void doGet(HttpServletRequest aRequest, HttpServletResponse aResponse) throws IOException, ServletException
   {
      processRequest(aRequest, aResponse);
   }
   public void  processRequest(HttpServletRequest aRequest, HttpServletResponse aResponse)  throws ServletException, IOException
   {
      if( checkSsl(aRequest, aResponse) ){
         return;
      }
      doProcessRequest(aRequest, aResponse);
   }
    private boolean checkSsl( HttpServletRequest aRequest, HttpServletResponse aResponse){
        boolean isSsl = getIsSsl();
        String redirectString = SslUtil.getRedirectString(aRequest, getServletContext(), isSsl);
        if( redirectString != null ){
            try{
                // Redirect the page to the desired URL
                aResponse.sendRedirect(
                    aResponse.encodeRedirectURL(redirectString));
                return true;
            }catch(Exception ioe){
               // Handle appropriately
            }
        }
        return false;
    }
   abstract protected void doProcessRequest(HttpServletRequest aRequest, HttpServletResponse aResponse)  throws ServletException;
   abstract protected boolean getIsSsl();
}

A class extending the BaseAppServlet class provides the implementation for the abstract methods doProcessRequest(), where the servlet completes its work, and getIsSsl(), which returns a flag indicating whether the servlet should be requested under HTTP or HTTPS.

Implementation within Struts

Based on the MVC design paradigm, Struts is a Web application framework developed within the Apache Software Foundation's Jakarta Project. Typically, in an MVC framework, a servlet, acting as controller, performs some operation, such as a database query. The servlet then forwards the data from the query, passing it to a JSP serving as the view for the data, or model. Struts defines a mechanism that allows for a deployment-time definition of the logical flow among Web resources using a configuration file and a servlet-like Action class. Struts also defines custom tag libraries that simplify form creation and processing, as well as bean component use within JSPs. To take advantage of these features, we will adapt our solution to work within the Struts framework.

The struts-config.xml configuration file

The struts-config.xml configuration file contains the mapping of the logical flows among Web resources. This file contains information about every action in the Web application formatted within a series of action tag elements. This information includes the Web resources to which an action forwards control and the conditions under which it does so.

The action element allows developers to use a parameter attribute for their own purposes. We use this attribute to specify whether a particular action utilizes HTTP or HTTPS. In our implementation, if the parameter attribute has a value of secure, then we'll process the action request via HTTPS. Otherwise, we'll use HTTP. Implementing the struts-config.xml file in this way allows us to change the protocols used by each action in the application at deployment time. One action element from a struts-config.xml file appears like this:

    <action path="/viewSignUp"
            type="com.someapp.action.ViewSignUpAction"
            parameter="secure" >
      <forward name="success" path="signupPage.jsp" />
    </action>

JSPs

JSPs within Struts differ from other JSPs only in the availability of the Struts custom tags. We implement our pageScheme custom tag in the same manner shown previously for ordinary JSPs.

Actions

Classes that extend the Struts-defined Action class handle the controller role within Struts. Each class must define a perform() method, which functions much like the doPost() and doGet() methods usually defined for a servlet. To implement calls to our redirect logic in Action-based classes, we again define a base class that every action in our Web application extends. The base class's logic determines whether an action needs to use HTTP or HTTPS. The base class will then use that information to call the redirect logic, as shown here:

package article.ssl.struts;
import org.apache.struts.action.*;
import javax.servlet.http.*;
import article.ssl.util.*;
public abstract class BaseAppAction extends Action {
    public static final String SECURE = "secure";
    public abstract ActionForward doPerform (
        ActionMapping aMapping,
        ActionForm aForm,
        HttpServletRequest aRequest,
        HttpServletResponse aResponse
    ) throws Exception;
    private boolean checkSsl(ActionMapping aMapping,
            HttpServletRequest aRequest,
            HttpServletResponse aResponse){
        String redirectString =
                SslUtil.getRedirectString(aRequest,
                getServlet().getServletContext(),
                SECURE.equals(aMapping.getParameter()));
        if( redirectString != null ){
            try{
                // Redirect the page to the desired URL
                aResponse.sendRedirect(
                    aResponse.encodeRedirectURL(redirectString));
                return true;
            }catch(Exception ioe){
                 // Handle appropriately
            }
        }
        return false;
    }
    public ActionForward perform(
        ActionMapping aMapping,
        ActionForm aForm,
        HttpServletRequest aRequest,
        HttpServletResponse aResponse
    ) {
        try {
            if( checkSsl(aMapping, aRequest, aResponse) ){
                return null;
            }
            ActionForward aAF =  doPerform(aMapping, aForm, aRequest, aResponse);
            return ((aAF == null) ? aMapping.findForward("success") : aAF);
        } catch (Exception e) {
            return aMapping.findForward("exception");
        }
    }
}
1 2 3 Page 2
Page 2 of 3