/* * Copyright 2011 cruxframework.org. * * Licensed 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.cruxframework.crux.core.server.dispatch.st; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.cruxframework.crux.core.shared.rpc.st.CruxSynchronizerTokenService; import org.cruxframework.crux.core.utils.ClassUtils; import com.google.gwt.user.server.Base64Utils; /** * @author Thiago da Rosa de Bustamante * */ public class CruxSynchronizerTokenServiceImpl implements CruxSynchronizerTokenService, CruxSynchronizerTokenHandler { private static final String EXPECTED_TOKENS_ATT = "__CRUX_SYNC_TOKEN_"; private static final String PROCESSING_TOKENS_ATT = "__CRUX_SYNC_TOKEN_IN_USE_"; private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private static Lock readLock = readWriteLock.readLock(); private static Lock writeLock = readWriteLock.writeLock(); private HttpServletRequest request; private HttpSession session; /** * @see org.cruxframework.crux.core.server.dispatch.RequestAware#setRequest(javax.servlet.http.HttpServletRequest) */ public void setRequest(HttpServletRequest request) { this.request = request; } /** * @see org.cruxframework.crux.core.server.dispatch.SessionAware#setSession(javax.servlet.http.HttpSession) */ public void setSession(HttpSession session) { this.session = session; } /** * @see org.cruxframework.crux.core.shared.rpc.st.CruxSynchronizerTokenService#getSynchronizerToken(java.lang.String) */ public String getSynchronizerToken(String methodFullSignature) { writeLock.lock(); try { if (createToken(methodFullSignature)) { return getExpectedToken(methodFullSignature); } } finally { writeLock.unlock(); } return null; } /** * @see org.cruxframework.crux.core.server.dispatch.st.CruxSynchronizerTokenHandler#isMethodRunning(java.lang.String) */ public boolean isMethodRunning(String methodFullSignature) { readLock.lock(); try { return getProcessingTokens().containsKey(methodFullSignature); } finally { readLock.unlock(); } } /** * @see org.cruxframework.crux.core.server.dispatch.st.CruxSynchronizerTokenHandler#startMethod(java.lang.String, javax.servlet.http.HttpServletRequest) */ public void startMethod(String methodFullSignature) throws InvalidTokenException { writeLock.lock(); try { String expectedToken = getExpectedToken(methodFullSignature); String receivedToken = request.getParameter(CRUX_SYNC_TOKEN_PARAM); if (expectedToken != null && receivedToken != null && expectedToken.equals(receivedToken)) { unregisterExpectedToken(methodFullSignature); registerProcessingToken(methodFullSignature); } else { throw new InvalidTokenException("Invalid Synchronizer Token for method ["+methodFullSignature+"]. Possible CSRF attack."); } } finally { writeLock.unlock(); } } private void registerProcessingToken(String methodFullSignature) { getProcessingTokens().put(methodFullSignature, true); forceProcessingTokensReplication(); } private void unregisterProcessingToken(String methodFullSignature) { getProcessingTokens().remove(methodFullSignature); forceProcessingTokensReplication(); } private void unregisterExpectedToken(String methodFullSignature) { getExpectedTokens().remove(methodFullSignature); forceExpectedTokensReplication(); } private void registerExpectedToken(String methodFullSignature, String token) { getExpectedTokens().put(methodFullSignature, token); forceExpectedTokensReplication(); } private String getExpectedToken(String methodSignature) { return getExpectedTokens().get(methodSignature); } /** * @see org.cruxframework.crux.core.server.dispatch.st.CruxSynchronizerTokenHandler#endMethod(java.lang.String) */ public void endMethod(String methodFullSignature) { writeLock.lock(); try { unregisterProcessingToken(methodFullSignature); } finally { writeLock.unlock(); } } /** * @see org.cruxframework.crux.core.server.dispatch.st.CruxSynchronizerTokenHandler#getMethodDescription(java.lang.reflect.Method) */ public String getMethodDescription(Method method) { return ClassUtils.getMethodDescription(method); } /** * Creates a new token for the requested method. If the method is already being processed * for the current user, return false and does not create the token. * @param methodFullSignature * @return true if the token was created. */ private boolean createToken(String methodFullSignature) { if (!isMethodRunning(methodFullSignature)) { String token = generateRandomToken(); registerExpectedToken(methodFullSignature, token); return true; } return false; } /** * Generates a random 256 bit token, coded as Base64. * @return */ private String generateRandomToken() { byte[] token = new byte[32]; new Random().nextBytes(token); return Base64Utils.toBase64(token); } /** * This is only necessary because Google AppEngine does not replicates sessions the same way other containers do. * The replication only occurs when you call the <code>setAttribute</code> method on the <code>session</code> object, * storing an object different than the previous one stored for the desired key. * <a href="https://developers.google.com/appengine/docs/java/config/appconfig#Enabling_Sessions">AppEngine Documentation</a> */ private void forceProcessingTokensReplication() { Map<String, Boolean> processing = getProcessingTokens(); Map<String, Boolean> clone = new HashMap<String, Boolean>(); clone.putAll(processing); session.setAttribute(PROCESSING_TOKENS_ATT, clone); } /** * This is only necessary because Google AppEngine does not replicates sessions the same way other containers do. * The replication only occurs when you call the <code>setAttribute</code> method on the <code>session</code> object, * storing an object different than the previous one stored for the desired key. * <a href="https://developers.google.com/appengine/docs/java/config/appconfig#Enabling_Sessions">AppEngine Documentation</a> */ private void forceExpectedTokensReplication() { Map<String, String> expected = getExpectedTokens(); Map<String, String> clone = new HashMap<String, String>(); clone.putAll(expected); session.setAttribute(EXPECTED_TOKENS_ATT, clone); } /** * @return */ @SuppressWarnings("unchecked") private Map<String, String> getExpectedTokens() { Map<String, String> tokens = (Map<String, String>) this.session.getAttribute(EXPECTED_TOKENS_ATT); if (tokens == null) { tokens = new HashMap<String, String>(); session.setAttribute(EXPECTED_TOKENS_ATT, tokens); } return tokens; } /** * @return */ @SuppressWarnings("unchecked") private Map<String, Boolean> getProcessingTokens() { Map<String, Boolean> inUsetokens = (Map<String, Boolean>) this.session.getAttribute(PROCESSING_TOKENS_ATT); if (inUsetokens == null) { inUsetokens = new HashMap<String, Boolean>(); session.setAttribute(PROCESSING_TOKENS_ATT, inUsetokens); } return inUsetokens; } }