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