classes/hirondelle/web4j/security/CsrfModifiedResponse.java
changeset 0 3060119b1292
equal deleted inserted replaced
-1:000000000000 0:3060119b1292
       
     1 package hirondelle.web4j.security;
       
     2 
       
     3 import hirondelle.web4j.model.Id;
       
     4 import hirondelle.web4j.util.EscapeChars;
       
     5 import hirondelle.web4j.util.Regex;
       
     6 import hirondelle.web4j.util.Util;
       
     7 
       
     8 import java.util.logging.Logger;
       
     9 import java.util.regex.Matcher;
       
    10 import java.util.regex.Pattern;
       
    11 
       
    12 import javax.servlet.http.HttpServletRequest;
       
    13 import javax.servlet.http.HttpServletResponse;
       
    14 import javax.servlet.http.HttpSession;
       
    15 
       
    16 /** Add a nonce to POSTed forms in any 'text/html' response. */
       
    17 final class CsrfModifiedResponse {
       
    18 
       
    19   CsrfModifiedResponse(HttpServletRequest aRequest, HttpServletResponse aResponse){
       
    20     fResponse = aResponse;
       
    21     fRequest = aRequest;
       
    22   }
       
    23   
       
    24   String addNonceTo(String aUnmodifiedResponse){
       
    25     String result = aUnmodifiedResponse;
       
    26     if(isServingHtml() && Util.textHasContent(aUnmodifiedResponse))  {
       
    27       fLogger.fine("Adding nonce to forms having method=POST, if any.");
       
    28       result = addHiddenParamToPostedForms(aUnmodifiedResponse);
       
    29     }
       
    30     return result;
       
    31   }
       
    32   
       
    33   // PRIVATE
       
    34   
       
    35   private HttpServletRequest fRequest;
       
    36   private HttpServletResponse fResponse;
       
    37 
       
    38   /**
       
    39     Group 1 is the FORM start tag *plus the body*, and group 2 is the FORM end tag.
       
    40     Note the reluctant qualifier for group 2, to ensure multiple forms are not glommed together.
       
    41   */
       
    42   private static final String REGEX =
       
    43      "(<form" + Regex.ALL_BUT_END_OF_TAG +"method=" + Regex.QUOTE + "POST" + Regex.QUOTE + Regex.ALL_BUT_END_OF_TAG + ">"  
       
    44      +  Regex.ANY_CHARS + "?)" + 
       
    45      "(</form>)"
       
    46   ; 
       
    47   private static final Pattern FORM_PATTERN = Pattern.compile(REGEX, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
       
    48 
       
    49   private static final String TEXT_HTML = "text/html";
       
    50   
       
    51   /**
       
    52    Problem: this class is package private. If we use the 'regular' logger, named for this class, then the output will not 
       
    53    show up. So, we use a logger attached to a closely related, public class. (Neat!) 
       
    54   */
       
    55   private static final Logger fLogger = Util.getLogger(CsrfFilter.class);
       
    56 
       
    57   /** Return true if content-type of reponse is null, or starts with 'text/html' (case-sensitive).  */
       
    58   private boolean isServingHtml(){
       
    59     String contentType = fResponse.getContentType();
       
    60     boolean missingContentType = ! Util.textHasContent(contentType);
       
    61     boolean startsWithHTML = Util.textHasContent(contentType) && contentType.startsWith(TEXT_HTML);
       
    62     return missingContentType || startsWithHTML;
       
    63   }
       
    64   
       
    65   private String addHiddenParamToPostedForms(String aOriginalInput) {
       
    66     StringBuffer result = new StringBuffer();
       
    67     Matcher formMatcher = FORM_PATTERN.matcher(aOriginalInput);
       
    68     while ( formMatcher.find() ){
       
    69       fLogger.fine("Found a POSTed form. Adding nonce.");
       
    70       formMatcher.appendReplacement(result, getReplacement(formMatcher));
       
    71     }
       
    72     formMatcher.appendTail(result);
       
    73     return result.toString();
       
    74   }
       
    75   
       
    76   private String getReplacement(Matcher aMatcher){
       
    77     //escape, since '$' char may appear in input
       
    78     return EscapeChars.forReplacementString(aMatcher.group(1) +  getHiddenInputTag() +  aMatcher.group(2));
       
    79   }
       
    80   
       
    81   private String getHiddenInputTag(){
       
    82     return "<input type='hidden' name='" + getHiddenParamName() + "' value='" + getHiddenParamValue().toString() + "'>";
       
    83   }
       
    84   
       
    85   /** 
       
    86    Return the form-source id value, stored in the user's session.
       
    87    If there is no session, or if there is no form-source id in the session, throw a RuntimeException.  
       
    88   */
       
    89   private Id getHiddenParamValue(){
       
    90     Id result = null;
       
    91     boolean DO_NOT_CREATE = false;
       
    92     HttpSession session = fRequest.getSession(DO_NOT_CREATE);
       
    93     if ( session != null ) {
       
    94       result = (Id)session.getAttribute(CsrfFilter.FORM_SOURCE_ID_KEY);
       
    95       if( result == null ){
       
    96         String message =  "Session exists, but no CSRF token value is stored in the session"; 
       
    97         fLogger.severe(message);
       
    98         throw new RuntimeException(message);
       
    99       }
       
   100     }
       
   101     else {
       
   102       String message = 
       
   103         "No session exists! CsrfFilter can only work when a session is present, and the user has logged in. " + 
       
   104         "Ensure CsrfFilter is mapped (using url-pattern) only to URLs having mandatory login and/or a valid session."
       
   105       ; 
       
   106       fLogger.severe(message);
       
   107       throw new RuntimeException(message);
       
   108     }
       
   109     return result;
       
   110   }
       
   111   
       
   112   /** Return the name of the hidden form parameter.  */
       
   113   private String getHiddenParamName(){
       
   114     return CsrfFilter.FORM_SOURCE_ID_KEY;
       
   115   }
       
   116 }