Initial cut at 2 cache implementations: one that uses servlet sessions and one that...
authorwassa <wassa@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Tue, 3 Oct 2006 17:57:42 +0000 (17:57 +0000)
committerwassa <wassa@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Tue, 3 Oct 2006 17:57:42 +0000 (17:57 +0000)
git-svn-id: https://subversion.switch.ch/svn/shibboleth/java-idp/trunk@2052 ab3bd59b-922f-494d-bb5f-6f0a3c29deca

src/edu/internet2/middleware/shibboleth/common/Cache.java
src/edu/internet2/middleware/shibboleth/common/CacheException.java [new file with mode: 0644]
src/edu/internet2/middleware/shibboleth/common/provider/BaseCache.java
src/edu/internet2/middleware/shibboleth/common/provider/CookieCache.java
src/edu/internet2/middleware/shibboleth/common/provider/ServletSessionCache.java
tests/edu/internet2/middleware/shibboleth/common/provider/CacheTests.java [new file with mode: 0644]

index ea50b29..c13cbb2 100644 (file)
@@ -29,9 +29,11 @@ public interface Cache {
 
        public CacheType getCacheType();
 
-       public Object retrieve(String key);
+       public String retrieve(String key) throws CacheException;
 
-       public boolean contains(String key);
+       public void remove(String key) throws CacheException;
 
-       public void store(String key, String value, long duration);
+       public boolean contains(String key) throws CacheException;
+
+       public void store(String key, String value, long duration) throws CacheException;
 }
diff --git a/src/edu/internet2/middleware/shibboleth/common/CacheException.java b/src/edu/internet2/middleware/shibboleth/common/CacheException.java
new file mode 100644 (file)
index 0000000..e65b42a
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
+ *
+ * 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 edu.internet2.middleware.shibboleth.common;
+
+public class CacheException extends Exception {
+
+       public CacheException(String message) {
+
+               super(message);
+       }
+}
index da99df0..eb61ac8 100644 (file)
@@ -32,6 +32,9 @@ public abstract class BaseCache implements Cache {
 
        protected BaseCache(String name, CacheType type) {
 
+               if (name == null || type == null) { throw new IllegalArgumentException(
+                               "Name and type are required for construction of BaseCache."); }
+
                this.name = name;
                this.cacheType = type;
        }
@@ -56,5 +59,11 @@ public abstract class BaseCache implements Cache {
                        this.value = value;
                        expiration = new Date(System.currentTimeMillis() + (duration * 1000));
                }
+
+               protected CacheEntry(String value, Date expireAt) {
+
+                       this.value = value;
+                       this.expiration = expireAt;
+               }
        }
 }
index 0269211..3a67170 100644 (file)
 
 package edu.internet2.middleware.shibboleth.common.provider;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
 import java.security.GeneralSecurityException;
-import java.security.KeyException;
 import java.security.SecureRandom;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
+import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
 
 import javax.crypto.Cipher;
@@ -36,47 +42,85 @@ import javax.crypto.spec.IvParameterSpec;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.apache.log4j.Logger;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
 
 import edu.internet2.middleware.shibboleth.common.Cache;
+import edu.internet2.middleware.shibboleth.common.CacheException;
 import edu.internet2.middleware.shibboleth.utils.Base32;
 
 /**
  * <code>Cache</code> implementation that uses browser cookies to store data. Symmetric and HMAC algorithms are used
  * to encrypt and verify the data. Due to the size limitations of cookie storage, data may interleaved among multiple
- * cookies.
+ * cookies. NOTE: Using this cache implementation in a standalon tomcat configuration will usually require that the
+ * "maxHttpHeaderSize" parameter be greatly increased.
  * 
  * @author Walter Hoehn
  */
 public class CookieCache extends BaseCache implements Cache {
 
-       // TODO domain limit?
+       private static Logger log = Logger.getLogger(CookieCache.class.getName());
        private HttpServletResponse response;
-       private List<Cookie> myCurrentCookies = new ArrayList<Cookie>();
+       private Collection<Cookie> myCurrentCookies = new ArrayList<Cookie>();
        private Map<String, CacheEntry> dataCache = new HashMap<String, CacheEntry>();
-       private static final int CHUNK_SIZE = 4 * 1024; // in KB, minimal browser requirement
+       private static final int CHUNK_SIZE = 4 * 1024; // minimal browser requirement
        private static final int COOKIE_LIMIT = 20; // minimal browser requirement
        private static final String NAME_PREFIX = "IDP_CACHE:";
+       private static int totalCookies = 0;
        protected SecretKey secret;
        private static SecureRandom random = new SecureRandom();
-       private String cipherAlgorithm = "DESede/CBC/PKCS5Padding";
-       private String macAlgorithm = "HmacSHA1";
-       private String storeType = "JCEKS";
+       private String cipherAlgorithm;
+       private String macAlgorithm;
 
-       CookieCache(String name, HttpServletRequest request, HttpServletResponse response) {
+       public CookieCache(String name, SecretKey key, String cipherAlgorithm, String macAlgorithm,
+                       HttpServletRequest request, HttpServletResponse response) throws CacheException {
 
                super(name, Cache.CacheType.CLIENT_SIDE);
+               this.secret = key;
+               this.cipherAlgorithm = cipherAlgorithm;
+               this.macAlgorithm = macAlgorithm;
                this.response = response;
                Cookie[] requestCookies = request.getCookies();
-               for (int i = 0; i < requestCookies.length; i++) {
-                       if (requestCookies[i].getName().startsWith(NAME_PREFIX)) {
-                               myCurrentCookies.add(requestCookies[i]);
+               if (requestCookies != null) {
+                       for (int i = 0; i < requestCookies.length; i++) {
+                               if (requestCookies[i].getName().startsWith(NAME_PREFIX + getName())
+                                               && requestCookies[i].getValue() != null) {
+                                       myCurrentCookies.add(requestCookies[i]);
+                               }
                        }
                }
 
-               // TODO dechunk, decrypt, and pull in dataCache
+               if (usingDefaultSecret()) {
+                       log.warn("You are running the Cookie Cache with the "
+                                       + "default secret key.  This is UNSAFE!  Please change "
+                                       + "this configuration and restart the IdP.");
+               }
+
+               initFromCookies();
+       }
+
+       public void postProcessing() throws CacheException {
+
+               if (totalCookies > (COOKIE_LIMIT - 1)) {
+                       log.warn("The Cookie Cache mechanism is about to write a large amount of data to the "
+                                       + "client.  This may not work with some browser software, so it is recommended"
+                                       + " that you investigate other caching mechanisms.");
+               }
+
+               flushCache();
        }
 
-       public boolean contains(String key) {
+       public boolean contains(String key) throws CacheException {
 
                CacheEntry entry = dataCache.get(key);
 
@@ -84,7 +128,9 @@ public class CookieCache extends BaseCache implements Cache {
 
                // Clean cache if it is expired
                if (new Date().after(((CacheEntry) entry).expiration)) {
-                       deleteFromCache(key);
+                       log.debug("Found expired object.  Deleting...");
+                       totalCookies--;
+                       dataCache.remove(key);
                        return false;
                }
 
@@ -92,13 +138,7 @@ public class CookieCache extends BaseCache implements Cache {
                return true;
        }
 
-       private void deleteFromCache(String key) {
-
-               dataCache.remove(key);
-               flushCache();
-       }
-
-       public Object retrieve(String key) {
+       public String retrieve(String key) throws CacheException {
 
                CacheEntry entry = dataCache.get(key);
 
@@ -106,7 +146,9 @@ public class CookieCache extends BaseCache implements Cache {
 
                // Clean cache if it is expired
                if (new Date().after(((CacheEntry) entry).expiration)) {
-                       deleteFromCache(key);
+                       log.debug("Found expired object.  Deleting...");
+                       totalCookies--;
+                       dataCache.remove(key);
                        return null;
                }
 
@@ -114,35 +156,180 @@ public class CookieCache extends BaseCache implements Cache {
                return entry.value;
        }
 
-       public void store(String key, String value, long duration) {
+       public void remove(String key) throws CacheException {
+
+               dataCache.remove(key);
+               totalCookies--;
+       }
+
+       public void store(String key, String value, long duration) throws CacheException {
 
                dataCache.put(key, new CacheEntry(value, duration));
-               flushCache();
+               totalCookies++;
+       }
+
+       private void initFromCookies() throws CacheException {
+
+               log.debug("Attempting to initialize cache from client-supplied cookies.");
+               // Pull data from cookies
+               List<Cookie> relevantCookies = new ArrayList<Cookie>();
+               for (Cookie cookie : myCurrentCookies) {
+                       if (cookie.getName().startsWith(NAME_PREFIX + getName())) {
+                               relevantCookies.add(cookie);
+                       }
+               }
+               if (relevantCookies.isEmpty()) {
+                       log.debug("No applicable cookies found.  Cache is empty.");
+                       return;
+               }
+
+               // Sort
+               String[] sortedCookieValues = new String[relevantCookies.size()];
+               for (Cookie cookie : relevantCookies) {
+                       String[] tokenizedName = cookie.getName().split(":");
+                       sortedCookieValues[Integer.parseInt(tokenizedName[tokenizedName.length - 1]) - 1] = cookie.getValue();
+               }
+               // Concatenate
+               StringBuffer concat = new StringBuffer();
+               for (String cookieValue : sortedCookieValues) {
+                       concat.append(cookieValue);
+               }
+               log.debug("Dumping Encrypted/Encoded Input Cache: " + concat);
+
+               try {
+                       // Decode Base32
+                       byte[] in = Base32.decode(concat.toString());
+
+                       // Decrypt
+                       Cipher cipher = Cipher.getInstance(cipherAlgorithm);
+                       int ivSize = cipher.getBlockSize();
+                       byte[] iv = new byte[ivSize];
+                       Mac mac = Mac.getInstance(macAlgorithm);
+                       mac.init(secret);
+                       int macSize = mac.getMacLength();
+                       if (in.length < ivSize) {
+                               log.error("Cache is malformed (not enough bytes).");
+                               throw new CacheException("Cache is malformed (not enough bytes).");
+                       }
+
+                       // extract the IV, setup the cipher and extract the encrypted data
+                       System.arraycopy(in, 0, iv, 0, ivSize);
+                       IvParameterSpec ivSpec = new IvParameterSpec(iv);
+                       cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
+                       byte[] encryptedData = new byte[in.length - iv.length];
+                       System.arraycopy(in, ivSize, encryptedData, 0, in.length - iv.length);
+
+                       // decrypt the rest of the data andsetup the streams
+                       byte[] decryptedBytes = cipher.doFinal(encryptedData);
+                       ByteArrayInputStream byteStream = new ByteArrayInputStream(decryptedBytes);
+                       GZIPInputStream compressedData = new GZIPInputStream(byteStream);
+                       ObjectInputStream dataStream = new ObjectInputStream(compressedData);
+
+                       // extract the components
+                       byte[] decodedMac = new byte[macSize];
+
+                       int bytesRead = dataStream.read(decodedMac);
+                       if (bytesRead != macSize) {
+                               log.error("Error parsing cache: Unable to extract HMAC.");
+                               throw new CacheException("Error parsing cache: Unable to extract HMAC.");
+                       }
+
+                       String decodedData = (String) dataStream.readObject();
+                       log.debug("Dumping Raw Input Cache: " + decodedData);
+
+                       // Verify HMAC
+                       byte[] generatedMac = mac.doFinal(decodedData.getBytes());
+                       if (!Arrays.equals(decodedMac, generatedMac)) {
+                               log.error("Cookie cache data failed integrity  check.");
+                               throw new GeneralSecurityException("Cookie cache data failed integrity check.");
+                       }
+
+                       // Parse XML
+                       DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+                       factory.setValidating(false);
+                       factory.setNamespaceAware(false);
+                       Element cacheElement = factory.newDocumentBuilder().parse(new InputSource(new StringReader(decodedData)))
+                                       .getDocumentElement();
+                       NodeList items = cacheElement.getElementsByTagName("Item");
+                       for (int i = 0; i < items.getLength(); i++) {
+                               Element item = (Element) items.item(i);
+                               totalCookies++;
+                               dataCache.put(item.getAttribute("key"), new CacheEntry(item.getAttribute("value"), new Date(new Long(
+                                               item.getAttribute("expire")))));
+                       }
+
+               } catch (Exception e) {
+                       log.error("Error decrypting cache data: " + e);
+                       throw new CacheException("Unable to read cached data.");
+               }
+       }
+
+       private boolean usingDefaultSecret() {
+
+               byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
+                               (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
+                               (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
+                               (byte) 0x61, (byte) 0xEF};
+               byte[] encodedKey = secret.getEncoded();
+               return Arrays.equals(defaultKey, encodedKey);
        }
 
        /**
         * Secures, encodes, and writes out (to cookies) cached data.
         */
-       private void flushCache() {
+       private void flushCache() throws CacheException {
+
+               log.debug("Flushing cache.");
+               log.debug("Encrypting cache data.");
 
-               // TODO create String representation of all cache data
+               // Create XML/String representation of all cache data
                String stringData = null;
 
                try {
 
+                       DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
+                       docFactory.setNamespaceAware(false);
+                       Document placeHolder = docFactory.newDocumentBuilder().newDocument();
+
+                       Element cacheNode = placeHolder.createElement("Cache");
+                       for (Entry<String, CacheEntry> entry : dataCache.entrySet()) {
+                               Element itemNode = placeHolder.createElement("Item");
+                               itemNode.setAttribute("key", entry.getKey());
+                               itemNode.setAttribute("value", entry.getValue().value);
+                               itemNode.setAttribute("expire", new Long(entry.getValue().expiration.getTime()).toString());
+                               cacheNode.appendChild(itemNode);
+                       }
+
+                       TransformerFactory factory = TransformerFactory.newInstance();
+                       DOMSource source = new DOMSource(cacheNode);
+                       Transformer transformer = factory.newTransformer();
+                       transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+                       StringWriter stringWriter = new StringWriter();
+                       StreamResult result = new StreamResult(stringWriter);
+                       transformer.transform(source, result);
+                       stringData = stringWriter.toString().replaceAll(">\\s<", "><");
+                       log.debug("Dumping Raw Cache: " + stringData);
+
+               } catch (Exception e) {
+                       log.error("Error encoding cache data: " + e);
+                       throw new CacheException("Unable to cache data.");
+               }
+
+               try {
+
                        // Setup a gzipped data stream
                        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
                        GZIPOutputStream compressedStream = new GZIPOutputStream(byteStream);
-                       DataOutputStream dataStream = new DataOutputStream(compressedStream);
+                       ObjectOutputStream dataStream = new ObjectOutputStream(compressedStream);
 
                        // Write data and HMAC to stream
                        Mac mac = Mac.getInstance(macAlgorithm);
                        mac.init(secret);
                        dataStream.write(mac.doFinal(stringData.getBytes()));
-                       dataStream.writeUTF(stringData);
+                       dataStream.writeObject(stringData);
 
                        // Flush
-                       dataStream.flush();
+                       // dataStream.flush();
                        compressedStream.flush();
                        compressedStream.finish();
                        byteStream.flush();
@@ -164,16 +351,14 @@ public class CookieCache extends BaseCache implements Cache {
 
                        // Base32 encode
                        String encodedData = Base32.encode(cacheBytes);
+                       log.debug("Dumping Encrypted/Encoded Cache: " + encodedData);
 
                        // Put into cookies
                        interleaveInCookies(encodedData);
 
-               } catch (KeyException e) {
-                       // TODO handle
-               } catch (GeneralSecurityException e) {
-                       // TODO handle
-               } catch (IOException e) {
-                       // TODO handle
+               } catch (Exception e) {
+                       log.error("Error encrypting cache data: " + e);
+                       throw new CacheException("Unable to cache data.");
                }
        }
 
@@ -182,45 +367,54 @@ public class CookieCache extends BaseCache implements Cache {
         */
        private void interleaveInCookies(String data) {
 
+               log.debug("Writing cache to cookies.");
+
                // Convert the String data to a list of cookies
-               List<Cookie> cookiesToResponse = new ArrayList<Cookie>();
+               Map<String, Cookie> cookiesToResponse = new HashMap<String, Cookie>();
                StringBuffer bufferredData = new StringBuffer(data);
+               int i = 1;
                while (bufferredData != null && bufferredData.length() > 0) {
                        Cookie cookie = null;
-                       String name = null;
-                       if (bufferredData.length() <= getCookieSpace()) {
+                       String name = NAME_PREFIX + getName() + ":" + i++;
+                       if (bufferredData.length() <= getCookieSpace(name)) {
                                cookie = new Cookie(name, bufferredData.toString());
                                bufferredData = null;
                        } else {
-                               cookie = new Cookie(name, bufferredData.substring(0, getCookieSpace() - 1));
-                               bufferredData.delete(0, getCookieSpace() - 1);
+                               cookie = new Cookie(name, bufferredData.substring(0, getCookieSpace(name) - 1));
+                               bufferredData.delete(0, getCookieSpace(name) - 1);
                        }
-                       cookiesToResponse.add(cookie);
+                       cookiesToResponse.put(cookie.getName(), cookie);
                }
 
-               // We have to null out cookies that are no longer needed
-               for (Cookie previousCookie : myCurrentCookies) {
-                       if (!cookiesToResponse.contains(previousCookie)) {
-                               cookiesToResponse.add(new Cookie(previousCookie.getName(), null));
+               // Expire cookies that we used previously but no longer need
+               for (Cookie currCookie : myCurrentCookies) {
+                       if (!cookiesToResponse.containsKey(currCookie.getName())) {
+                               currCookie.setMaxAge(0);
+                               currCookie.setValue(null);
+                               cookiesToResponse.put(currCookie.getName(), currCookie);
                        }
                }
 
                // Write our cookies to the response object
-               for (Cookie cookie : cookiesToResponse) {
+               for (Cookie cookie : cookiesToResponse.values()) {
                        response.addCookie(cookie);
                }
 
                // Update our cached copy of the cookies
-               myCurrentCookies = cookiesToResponse;
+               myCurrentCookies = cookiesToResponse.values();
        }
 
        /**
         * Returns the amount of value space available in cookies we create
         */
-       private int getCookieSpace() {
-
-               // TODO this needs to be better
-               return 3000;
+       private int getCookieSpace(String cookieName) {
+
+               // If we add other cookie variables, we would need to adjust this algorithm appropriately
+               StringBuffer used = new StringBuffer();
+               used.append("Set-Cookie: ");
+               used.append(cookieName + "=" + " ");
+               System.err.println(CHUNK_SIZE - used.length() - 2);
+               return CHUNK_SIZE - used.length() - 2;
        }
 
 }
index 79cd5c0..483ca9f 100644 (file)
@@ -21,7 +21,10 @@ import java.util.Date;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 
+import org.apache.log4j.Logger;
+
 import edu.internet2.middleware.shibboleth.common.Cache;
+import edu.internet2.middleware.shibboleth.common.CacheException;
 
 /**
  * <code>Cache</code> implementation that uses Servlet API sessions to cache data. This implementation will reap
@@ -32,11 +35,16 @@ import edu.internet2.middleware.shibboleth.common.Cache;
  */
 public class ServletSessionCache extends BaseCache implements Cache {
 
-       HttpSession session;
+       private static Logger log = Logger.getLogger(ServletSessionCache.class.getName());
+       private HttpSession session;
 
        ServletSessionCache(String name, HttpServletRequest request) {
 
                super(name, Cache.CacheType.CLIENT_SERVER_SHARED);
+
+               if (request == null) { throw new IllegalArgumentException(
+                               "Servlet request is  required for construction of BaseCache."); }
+
                this.session = request.getSession();
        }
 
@@ -53,6 +61,7 @@ public class ServletSessionCache extends BaseCache implements Cache {
 
                // Clean cache if it is expired
                if (new Date().after(((CacheEntry) object).expiration)) {
+                       log.debug("Found expired object.  Deleting...");
                        session.removeAttribute(getInternalKeyName(key));
                        return false;
                }
@@ -69,6 +78,7 @@ public class ServletSessionCache extends BaseCache implements Cache {
 
                // Clean cache if it is expired
                if (new Date().after(((CacheEntry) object).expiration)) {
+                       log.debug("Found expired object.  Deleting...");
                        session.removeAttribute(getInternalKeyName(key));
                        return null;
                }
@@ -82,4 +92,9 @@ public class ServletSessionCache extends BaseCache implements Cache {
                session.setAttribute(getInternalKeyName(key), new CacheEntry(value, duration));
        }
 
+       public void remove(String key) throws CacheException {
+
+               session.removeAttribute(getInternalKeyName(key));
+       }
+
 }
diff --git a/tests/edu/internet2/middleware/shibboleth/common/provider/CacheTests.java b/tests/edu/internet2/middleware/shibboleth/common/provider/CacheTests.java
new file mode 100644 (file)
index 0000000..14533d5
--- /dev/null
@@ -0,0 +1,249 @@
+/*
+ * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
+ *
+ * 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 edu.internet2.middleware.shibboleth.common.provider;
+
+import java.util.List;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import javax.servlet.http.Cookie;
+
+import junit.framework.TestCase;
+
+import org.apache.log4j.BasicConfigurator;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+
+import com.mockrunner.mock.web.MockHttpServletRequest;
+import com.mockrunner.mock.web.MockHttpServletResponse;
+import com.mockrunner.mock.web.WebMockObjectFactory;
+
+import edu.internet2.middleware.shibboleth.common.Cache;
+import edu.internet2.middleware.shibboleth.common.CacheException;
+import edu.internet2.middleware.shibboleth.common.CredentialsTests;
+
+public class CacheTests extends TestCase {
+
+       private static Logger log = Logger.getLogger(CacheTests.class.getName());
+       private WebMockObjectFactory factory = new WebMockObjectFactory();
+       private MockHttpServletResponse response = factory.getMockResponse();
+       private MockHttpServletRequest request = factory.getMockRequest();
+
+       private String cipherAlgorithm = "DESede/CBC/PKCS5Padding";
+       private String macAlgorithm = "HmacSHA1";
+
+       byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
+                       (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
+                       (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
+                       (byte) 0x61, (byte) 0xEF};
+
+       public CacheTests(String name) {
+
+               super(name);
+               BasicConfigurator.resetConfiguration();
+               BasicConfigurator.configure();
+               Logger.getRootLogger().setLevel(Level.DEBUG);
+       }
+
+       public static void main(String[] args) {
+
+               junit.textui.TestRunner.run(CredentialsTests.class);
+               BasicConfigurator.configure();
+               Logger.getRootLogger().setLevel(Level.OFF);
+       }
+
+       /**
+        * @see junit.framework.TestCase#setUp()
+        */
+       protected void setUp() throws Exception {
+
+               super.setUp();
+               request.resetAll();
+               response.resetAll();
+       }
+
+       public void testServletSessionCache() {
+
+               try {
+                       // Startup the cache
+                       Cache cache = new ServletSessionCache("foobar", request);
+
+                       // Make sure the cache starts clean
+                       assertNull("Cache contained errant record.", cache.retrieve("foo"));
+
+                       // Store and retrieve
+                       cache.store("foo", "bar", 99999);
+                       assertTrue("Cache expected to contain record.", cache.contains("foo"));
+                       assertEquals("Cache expected to contain record.", "bar", cache.retrieve("foo"));
+
+                       // Make sure expiration works
+                       cache.store("bar", "foo", 1);
+                       try {
+                               Thread.sleep(2000);
+                       } catch (InterruptedException e) {
+                               // Who cares
+                       }
+                       assertFalse("Cache expected to expire record.", cache.contains("bar"));
+                       assertEquals("Cache expected to expire record.", null, cache.retrieve("bar"));
+
+               } catch (CacheException e) {
+                       fail("Error exercising cache: " + e);
+               }
+       }
+
+       public void testCookieCacheBasic() {
+
+               try {
+
+                       SecretKey secret = new SecretKeySpec(defaultKey, "DESede");
+
+                       // Startup the cache
+                       CookieCache cache = new CookieCache("foobar", secret, cipherAlgorithm, macAlgorithm, request, response);
+
+                       // Make sure the cache starts clean
+                       assertNull("Cache contained errant record.", cache.retrieve("foo"));
+
+                       // Store and retrieve
+                       cache.store("foo", "bar", 99999);
+                       assertTrue("Cache expected to contain record.", cache.contains("foo"));
+                       assertEquals("Cache expected to contain record.", "bar", cache.retrieve("foo"));
+
+                       // Make sure expiration works
+                       cache.store("expr1", "foo", 1); // check immediate
+                       cache.store("expr2", "foo", 1); // check after round trip
+
+                       try {
+                               Thread.sleep(2000);
+                       } catch (InterruptedException e) {
+                               // Who cares
+                       }
+                       assertFalse("Cache expected to expire record.", cache.contains("expr1"));
+                       assertEquals("Cache expected to expire record.", null, cache.retrieve("expr1"));
+
+                       // Write cache to cookies
+                       cache.postProcessing();
+                       request.resetAll();
+
+                       // Round trip testing
+                       // Add cookies from previous query response to the new request.. to simulate a browser interaction
+                       List<Cookie> cookies = (List<Cookie>) response.getCookies();
+                       for (Cookie cookie : cookies) {
+                               log.debug("Cookie Name: " + cookie.getName());
+                               log.debug("Cookie Value: " + cookie.getValue());
+                               request.addCookie(cookie);
+                       }
+
+                       response.resetAll();
+
+                       cache = new CookieCache("foobar", secret, cipherAlgorithm, macAlgorithm, request, response);
+
+                       // Test round-tripped entry
+                       assertTrue("Cache expected to contain record.", cache.contains("foo"));
+                       assertEquals("Cache expected to contain record.", "bar", cache.retrieve("foo"));
+
+                       // Test round-tripped expired entry
+                       assertFalse("Cache expected to expire record.", cache.contains("expr2"));
+                       assertEquals("Cache expected to expire record.", null, cache.retrieve("expr2"));
+
+               } catch (CacheException e) {
+                       fail("Error exercising cache: " + e);
+               }
+       }
+
+       public void testCookieCacheLargeDataSet() {
+
+               try {
+
+                       SecretKey secret = new SecretKeySpec(defaultKey, "DESede");
+
+                       // Round trip with a large data set
+                       CookieCache cache = new CookieCache("foobar", secret, cipherAlgorithm, macAlgorithm, request, response);
+                       for (int i = 0; i < 5000; i++) {
+                               cache.store(new Integer(i).toString(), "Walter", 99999);
+                       }
+
+                       cache.postProcessing();
+                       request.resetAll();
+
+                       List<Cookie> cookies = (List<Cookie>) response.getCookies();
+                       for (Cookie cookie : cookies) {
+                               log.debug("Cookie Name: " + cookie.getName());
+                               log.debug("Cookie Value: " + cookie.getValue());
+                               request.addCookie(cookie);
+                       }
+
+                       response.resetAll();
+
+                       cache = new CookieCache("foobar", secret, cipherAlgorithm, macAlgorithm, request, response);
+                       assertEquals("Cache expected to contain record.", "Walter", cache.retrieve(new Integer(1).toString()));
+
+               } catch (CacheException e) {
+                       fail("Error exercising cache: " + e);
+               }
+       }
+
+       public void testCookieCacheStaleCookieCleanup() {
+
+               try {
+
+                       SecretKey secret = new SecretKeySpec(defaultKey, "DESede");
+
+                       // Round trip with a large data set
+                       CookieCache cache = new CookieCache("foobar", secret, cipherAlgorithm, macAlgorithm, request, response);
+                       for (int i = 0; i < 5000; i++) {
+                               cache.store(new Integer(i).toString(), "Walter", 99999);
+                       }
+
+                       cache.postProcessing();
+                       request.resetAll();
+
+                       List<Cookie> cookies = (List<Cookie>) response.getCookies();
+                       for (Cookie cookie : cookies) {
+                               log.debug("Cookie Name: " + cookie.getName());
+                               log.debug("Cookie Value: " + cookie.getValue());
+                               log.debug("Cookie Max Age: " + cookie.getMaxAge());
+                               request.addCookie(cookie);
+                       }
+
+                       response.resetAll();
+                       cache = new CookieCache("foobar", secret, cipherAlgorithm, macAlgorithm, request, response);
+
+                       // OK, delete a bunch of entries and make sure this is reflected in the cookies
+                       for (int i = 0; i < 4999; i++) {
+                               cache.remove(new Integer(i).toString());
+                       }
+
+                       cache.postProcessing();
+                       request.resetAll();
+
+                       cookies = (List<Cookie>) response.getCookies();
+                       for (Cookie cookie : cookies) {
+                               log.debug("Cookie Name: " + cookie.getName());
+                               log.debug("Cookie Value: " + cookie.getValue());
+                               log.debug("Cookie Max Age: " + cookie.getMaxAge());
+                               request.addCookie(cookie);
+                               if (!cookie.getName().equals("IDP_CACHE:foobar:1")) {
+                                       assertTrue("Cookie not properly expired.", cookie.getMaxAge() == 0);
+                               }
+                       }
+
+               } catch (CacheException e) {
+                       fail("Error exercising cache: " + e);
+               }
+       }
+
+}