/*
* Copyright (C) 2000 - 2008 TagServlet Ltd
*
* This file is part of Open BlueDragon (OpenBD) CFML Server Engine.
*
* OpenBD is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* Free Software Foundation,version 3.
*
* OpenBD is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenBD. If not, see http://www.gnu.org/licenses/
*
* Additional permission under GNU GPL version 3 section 7
*
* If you modify this Program, or any covered work, by linking or combining
* it with any of the JARS listed in the README.txt (or a modified version of
* (that library), containing parts covered by the terms of that JAR, the
* licensors of this Program grant you additional permission to convey the
* resulting work.
* README.txt @ http://www.openbluedragon.org/license/README.txt
*
* http://www.openbluedragon.org/
*/
package com.nary.security;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import com.nary.util.FastMap;
import com.naryx.tagfusion.cfm.engine.cfEngine;
/**
* This class stores security roles information for users logged-in via the
* <cfloginuser> tag. It's shutdown method will clean out expired data.
*
* This class facilitates the storing and reading in of security roles for
* logged in users, to/from a back-end store (i.e. across restarts). Currently
* the only supported type of back-end store is a file on the hard-drive.
*
* This cache will: 1. attempt to read in previously persisted entries from
* disk, during startup 2. persist all its non-expired entries to disk at
* shutdown
*
* This cache does not: 1. impose a maximum number of entries to manage memory
* usage 2. do any sort of runtime swapping of entries between RAM and back-end
* store 3. do any sort of periodic/Threaded cleanup of old, stale, expired
* entries during runtime
*
* So it is a potential memory "leak".
*/
public class SecurityCache {
public static final String STORAGE_TYPE_KEY = "storageType";
public static final String STORAGE_LOCATION_KEY = "storageFolderLocation";
public static final String STORAGE_FILENAME = "securityRoles.data";
String storageType, storageLocation;
Map<String, SecurityCacheItem> data = null;
int maxResidents;
public SecurityCache(int maxResidents, Properties props) {
this.maxResidents = maxResidents;
init(props);
}
private void init(Properties config) {
storageType = config.getProperty(STORAGE_TYPE_KEY);
storageLocation = config.getProperty(STORAGE_LOCATION_KEY);
// read-in/deserialize any persisted data
read();
if (data == null)
data = new FastMap<String, SecurityCacheItem>();
else
removeExpiredEntries();
}
public Map<String, String> get(String key) {
SecurityCacheItem o = null;
Map<String, String> roles = null;
o = data.get(key);
if (o != null) {
SecurityCacheItem item = o;
if (!isExpired(item))
roles = item.getData();
}
return roles;
}
public void add(String key, Map<String, String> val, long lifespan) {
data.put(key, new SecurityCacheItem(val, lifespan));
}
public void remove(String key) {
data.remove(key);
}
private void read() {
File f = new File(storageLocation);
if (f.exists()) {
FileInputStream fin = null;
ObjectInputStream in = null;
try {
// Create input streams for the data
fin = new FileInputStream(f);
in = new ObjectInputStream(fin);
data = (Map<String, SecurityCacheItem>) in.readObject();
} catch (Exception e) {
} finally {
// make certain that the input streams get closed
if (in != null) {
try {
in.close();
in = null;
} catch (IOException ioe) {
}
}
if (fin != null) {
try {
fin.close();
fin = null;
} catch (IOException ioe) {
}
}
}
}
}
public synchronized void persist() {
File f = new File(storageLocation);
if (data != null && data.size() > 0) {
// make sure that the parent folder exists
File parentFolder = f.getParentFile();
if (!parentFolder.exists())
parentFolder.mkdirs();
OutputStream fout = null;
ObjectOutputStream out = null;
try {
fout = cfEngine.thisPlatform.getFileIO().getFileOutputStream(f);
out = new ObjectOutputStream(fout);
// Write out the data
out.writeObject(data);
out.close();
fout.close();
} catch (Exception e) {
// Since it wasn't written out properly we should delete it
f.delete();
} finally {
// make certain that the streams get closed
if (out != null) {
try {
out.close();
out = null;
} catch (IOException ioe) {
}
}
if (fout != null) {
try {
fout.close();
fout = null;
} catch (IOException ioe) {
}
}
}
}
}
private static boolean isExpired(SecurityCacheItem item) {
boolean isExpired = false;
long lifespan = item.getLifespan();
// modified a condition here to fix bug #1775
isExpired = (lifespan == 0 || (lifespan > 0 && (item.getCreationTime() + lifespan <= System.currentTimeMillis())));
return isExpired;
}
public synchronized void removeExpiredEntries() {
Iterator<String> it = data.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
SecurityCacheItem item = data.get(key);
Map<String, String> roles = item.getData();
if (roles.isEmpty() || isExpired(item))
it.remove();
}
}
public synchronized void shutdown() {
// first clean out all expired entries
removeExpiredEntries();
// now persist the roles
persist();
// now remove all in-memory entries
data.clear();
}
}// end SecurityCache class
/*****************/
/** inner class **/
/*****************/
class SecurityCacheItem implements java.io.Serializable {
static final long serialVersionUID = 1;
long creationTime = System.currentTimeMillis();
Map<String, String> data = null;
long lifespan = -1; // -1 = lives forever, 0 = dies ASAP
/**
*
* @param data
* - a com.naryx.tagfusion.cfm.engine.cfJavaObjectData instance which
* holds a Map whose keys are the roles specified by the roles
* attribute of the <cfloginuser> tag
* @param lifespan
* - if cookies are used to track login data then this is the value
* of the idleTimeout attribute of the <cflogin> tag (converted to
* milliseconds) (actually not anymore since fixing bug #2309), else
* the login data is being tracked with the J2EE session, in which
* case this value is -1. -1 is used to signify that the lifespan of
* this cache item is not known at this time (sessions keep
* refreshing as requests continue to come in). In this case, the
* HttpSessionBindingListener interface COULD used by
* SessionLoginToken, in tandem with
* SecurityCache.removeExpiredEntries() to handle the expiring of
* this item, but that's not implemeted at this time.
*/
SecurityCacheItem(Map<String, String> data, long lifespan) {
this.data = data;
this.lifespan = lifespan;
}
public long getCreationTime() {
return creationTime;
}
public long getLifespan() {
return lifespan;
}
public Map<String, String> getData() {
return data;
}
}