/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2005 Sun Microsystems Inc. All Rights Reserved
*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* https://opensso.dev.java.net/public/CDDLv1.0.html or
* opensso/legal/CDDLv1.0.txt
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at opensso/legal/CDDLv1.0.txt.
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* $Id: Session.java,v 1.25 2009/08/14 17:53:35 weisun2 Exp $
*
* Portions copyright 2010-2016 ForgeRock AS.
*/
package com.iplanet.dpro.session;
import static org.forgerock.openam.session.SessionConstants.*;
import static org.forgerock.openam.utils.Time.*;
import java.net.URL;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.forgerock.guice.core.InjectorHolder;
import org.forgerock.openam.blacklist.BlacklistException;
import org.forgerock.openam.blacklist.Blacklistable;
import org.forgerock.openam.session.AMSession;
import org.forgerock.openam.session.SessionCache;
import org.forgerock.openam.session.SessionConstants;
import org.forgerock.openam.session.SessionCookies;
import org.forgerock.openam.session.SessionPLLSender;
import org.forgerock.openam.session.SessionServiceURLService;
import org.forgerock.util.Reject;
import org.forgerock.util.annotations.VisibleForTesting;
import com.iplanet.am.util.SystemProperties;
import com.iplanet.dpro.session.operations.ClientSdkSessionOperationStrategy;
import com.iplanet.dpro.session.operations.ServerSessionOperationStrategy;
import com.iplanet.dpro.session.operations.SessionOperationStrategy;
import com.iplanet.dpro.session.operations.SessionOperations;
import com.iplanet.dpro.session.operations.strategies.ClientSdkOperations;
import com.iplanet.dpro.session.service.SessionState;
import com.iplanet.dpro.session.service.SessionType;
import com.iplanet.dpro.session.share.SessionBundle;
import com.iplanet.dpro.session.share.SessionInfo;
import com.iplanet.services.comm.client.PLLClient;
import com.iplanet.services.naming.WebtopNaming;
import com.iplanet.sso.SSOException;
import com.iplanet.sso.SSOToken;
import com.sun.identity.common.SearchResults;
import com.sun.identity.session.util.RestrictedTokenAction;
import com.sun.identity.session.util.RestrictedTokenContext;
import com.sun.identity.shared.debug.Debug;
/**
* The Session
class represents a session. It contains session
* related information such as session ID, session type (user/application),
* client ID (user ID or application ID), session idle time, time left on the
* session, and session state. It also allows applications to add listener for
* session events.
*
* @see com.iplanet.dpro.session.SessionID
* @see com.iplanet.dpro.session.SessionListener
*/
public class Session implements Blacklistable, AMSession{
public static final String CACHED_BASE_POLLING_PROPERTY = "com.iplanet.am.session.client.polling.cacheBased";
private static final Debug sessionDebug = Debug.getInstance(SessionConstants.SESSION_DEBUG);
private final SessionCookies sessionCookies;
private final SessionCache sessionCache;
private final SessionServiceURLService sessionServiceURLService;
/*
* Used instead of SessionService in order to avoid potential issues in the ClientSDK. Should eventually not be used
* here.
*/
private final SessionOperationStrategy sessionOperationStrategy;
/**
* Defines the type of Session that has been created.
*/
private SessionType sessionType = SessionType.USER;
/**
* Identification string of the Client using this Session.
*/
private String clientID;
/**
* Name of the Domain or Organization through which the User/Client has been
* Authenticated.
*/
private String clientDomain;
/**
* Total Maximum time allowed for the session, in minutes.
*/
private long maxSessionTime;
/**
* Maximum idle time allowed for the session, in minutes.
*/
private long maxIdleTime;
/**
* Maximum time for which the cached session is used, in minutes.
*/
private long maxCachingTime;
/**
* The time for which the session has been idle, in seconds.
*/
private long sessionIdleTime;
/**
* Total time left for the session, in seconds.
*/
private long sessionTimeLeft;
/**
* Time after which the session will be considered invalid, in milliseconds.
*/
private long sessionExpiryTime;
/*
* This is the time value (computed as System.currentTimeMillis()/1000) when
* the session timed out. Value zero means the session has not timed out.
*/
private volatile long timedOutAt = 0;
/**
* Four possible values for the state of the session:
*
true
if session timed out,
* false
otherwise
* @exception SessionException
*/
public boolean isTimedOut() throws SessionException {
/**
* Before going to the server, check if the session has been already
* marked TimedOut or not.
*/
if (timedOutAt > 0) {
return true;
}
if (!usingCachedBasedPolling() && maxCachingTimeReached()){
try {
refresh(false);
} catch (SessionTimedOutException e) {
latestRefreshTime = currentTimeMillis() / 1000;
timedOutAt = latestRefreshTime;
}
}
return timedOutAt > 0;
}
/**
* Returns the maximum session caching time in minutes.
*
* @return The maximum session caching time.
*/
public long getMaxCachingTime() {
return maxCachingTime;
}
/**
* Returns the session idle time in seconds.
*
* @return The session idle time.
* @exception SessionException if the session reached its maximum session
* time, or the session was destroyed, or there was an error
* during communication with session service.
*/
public long getIdleTime() throws SessionException {
refreshSessionIfStale();
return sessionIdleTime;
}
/**
* Returns the time left for this session in seconds.
*
* @return The time left for this session.
* @exception SessionException is thrown if the session reached its
* maximum session time, or the session was destroyed, or
* there was an error during communication with session
* service.
*/
public long getTimeLeft() throws SessionException {
refreshSessionIfStale();
return sessionTimeLeft;
}
private void refreshSessionIfStale() throws SessionException {
if (!usingCachedBasedPolling() && maxCachingTimeReached()) {
refresh(false);
}
}
/**
* The time (in milliseconds from the UTC epoch) until this session can be removed from a session blacklist. This
* is guaranteed to be some time after the session has expired.
*
* @return the at which the session expires (if it has not already) plus a purge delay.
* @throws BlacklistException if the session has already expired or an error occurs.
*/
@Override
public long getBlacklistExpiryTime() throws BlacklistException {
try {
refreshSessionIfStale();
return sessionExpiryTime;
} catch (SessionException e) {
throw new BlacklistException(e);
}
}
/**
* Gets the time at which the Session was last refreshed from the master copy (in millis).
* @return The latest time at which the session was refreshed.
*/
public long getLatestRefreshTime() {
return latestRefreshTime;
}
/**
* Returns the state of the session.
*
* @param reset
* This parameter indicates that whether the Session Service
* needs to reset the latest access time on this session.
* @return The state of the session. The session state is one of the
* following: INVALID, VALID, INACTIVE, and DESTROYED
.
* @exception SessionException is thrown if the session reached its
* maximum session time, or the session was destroyed, or
* there was an error during communication with session
* service.
*/
public SessionState getState(boolean reset) throws SessionException {
if (!usingCachedBasedPolling() && maxCachingTimeReached()) {
refresh(reset);
} else {
if (reset) {
needToReset = true;
}
}
return sessionState;
}
public void setState(SessionState state) {
sessionState = state;
}
/**
* Gets the property stored in this session.
*
* @param name The property name.
* @return The property value in String format.
* @exception SessionException is thrown if the session reached its
* maximum session time, or the session was destroyed, or
* there was an error during communication with session
* service.
*/
public String getProperty(String name) throws SessionException {
if (name == null ? sessionCookies.getLBCookieName() != null : !name.equals(sessionCookies.getLBCookieName())) {
if ((!usingCachedBasedPolling() && maxCachingTimeReached()) ||
!sessionProperties.containsKey(name)) {
refresh(false);
}
}
return sessionProperties.get(name);
}
/**
* Given a restricted token, returns the SSOTokenID of the master token
* can only be used if the requester is an app token
*
* This functionality is only available in server mode.
*
* @param session Must be an app token
* @param restrictedId The SSOTokenID of the restricted token
* @return The SSOTokenID string of the master token
* @throws SSOException If the master token cannot be dereferenced
*/
public String dereferenceRestrictedTokenID(Session session, String restrictedId) throws SessionException {
String masterSID;
try {
SessionOperations operations = sessionOperationStrategy.getOperation(session.getSessionID());
masterSID = operations.deferenceRestrictedID(session, new SessionID(restrictedId));
} catch (Exception e) {
sessionDebug.error("unable to find master token for " + restrictedId, e);
throw new SessionException(e);
}
return masterSID;
}
/**
* Returns true if the SSOTokenID associated with this SSOToken is a
* restricted token, false otherwise.
*
* @return true if the token is restricted
* @throws SSOException If we are unable to determine if the session is
* restricted
*/
public boolean isRestricted() throws SessionException {
return restriction != null;
}
/**
* Gets the property stored in this session.
*
* @param name The property name.
* @return The property value in String format only
* when run in the server mode else return null
*/
public String getPropertyWithoutValidation(String name) {
if (SystemProperties.isServerMode()) {
return sessionProperties.get(name);
}
return null;
}
/**
* Sets a property for this session.
*
* @param name The property name.
* @param value The property value.
* @exception SessionException if the session reached its maximum session
* time, or the session was destroyed, or there was an error
* during communication with session service, or if the property
* name or value was null.
*/
public void setProperty(String name, String value) throws SessionException {
if (name == null || value == null) {
throw new SessionException("Session property name/value cannot be null");
}
try {
SessionOperations operation = sessionOperationStrategy.getOperation(this.getID());
operation.setProperty(this, name, value);
sessionProperties.put(name, value);
} catch (Exception e) {
throw new SessionException(e);
}
}
/**
* Used to find out if the maximum caching time has reached or not.
*/
public boolean maxCachingTimeReached() {
long cachingtime = currentTimeMillis() / 1000 - latestRefreshTime;
return cachingtime > maxCachingTime * 60;
}
/**
* Gets the Session Service URL for this session object.
*
* @return The Session Service URL for this session.
* @exception SessionException when cannot get Session URL.
*/
public URL getSessionServiceURL() throws SessionException {
if (SystemProperties.isServerMode()) {
return sessionServiceURLService.getSessionServiceURL(sessionID);
}
// we can cache the result because in client mode
// session service location does not change
// dynamically
if (sessionServiceURL == null) {
sessionServiceURL = sessionServiceURLService.getSessionServiceURL(sessionID);
}
return sessionServiceURL;
}
/**
* Destroys a session.
*
* @param session The session to be destroyed.
* @exception SessionException if there was an error during
* communication with session service, or the corresponding
* session reached its maximum session/idle time, or the
* session was destroyed.
*/
public void destroySession(Session session) throws SessionException {
try {
SessionOperations operation = sessionOperationStrategy.getOperation(this.getID());
operation.destroy(this, session);
} catch (Exception e) {
throw new SessionException(e);
}
finally {
sessionCache.removeSID(session.getID());
}
}
/**
* Logs out a session.
*
* @throws SessionException if there was an error during communication
* with session service. If the session logged out already,
* no exception will be thrown.
*/
public void logout() throws SessionException {
try {
SessionOperations operation = sessionOperationStrategy.getOperation(this.getID());
operation.logout(this);
sessionCache.removeSID(sessionID);
} catch (Exception e) {
throw new SessionException(e);
}
}
/**
* Adds a session listener for session change events.
*
* @param listener Session Listener object.
* @exception SessionException if the session state is not valid.
*/
public void addSessionListener(SessionListener listener) throws SessionException {
addSessionListener(listener, false);
}
/**
* Adds a session listener for session change events.
*
* @param listener Session Listener object.
* @param force whether to ignore whether a Session is in the Invalid state. If false will throw an exception if
* the Session is Invalid.
* @exception SessionException if the session state is not valid.
*/
public void addSessionListener(SessionListener listener, boolean force) throws SessionException {
if (!force && sessionState != SessionState.VALID) {
throw new SessionException(SessionBundle.rbName, "invalidSessionState", null);
}
localSessionEventListeners.add(listener);
}
/**
* Gets all valid sessions from the specified session server. This session
* is subject to access control in order to get all sessions.
*
* @param server
* The session server name. If the server name contains protocol
* and port, the protocol and port will be used. Otherwise, the
* server protocol and port is default to the same protocol and
* port of the calling session.
* @return A Vector of Session objects.
* @exception SessionException if there was an error during
* communication with session service.
*/
public SearchResultstrue
refreshes the Session Information
*/
private void doRefresh(boolean reset) throws SessionException {
boolean flag = reset || needToReset;
needToReset = false;
SessionOperations operation = sessionOperationStrategy.getOperation(this.getID());
SessionInfo info = operation.refresh(this, flag);
long oldMaxCachingTime = maxCachingTime;
long oldMaxIdleTime = maxIdleTime;
long oldMaxSessionTime = maxSessionTime;
update(info);
sessionCache.notifySessionRefresh(this, oldMaxCachingTime, oldMaxIdleTime, oldMaxSessionTime);
}
/**
* Updates the session from the session information server.
*
* @param info Session Information.
*/
public synchronized void update(SessionInfo info) throws SessionException {
if (info.getSessionType().equals("user")) {
sessionType = SessionType.USER;
} else if (info.getSessionType().equals("application")) {
sessionType = SessionType.APPLICATION;
}
clientID = info.getClientID();
clientDomain = info.getClientDomain();
maxSessionTime = info.getMaxTime();
maxIdleTime = info.getMaxIdle();
maxCachingTime = info.getMaxCaching();
sessionIdleTime = info.getTimeIdle();
sessionExpiryTime = info.getExpiryTime(TimeUnit.MILLISECONDS);
sessionTimeLeft = info.getTimeLeft();
sessionState = SessionState.valueOf(info.getState().toUpperCase());
sessionProperties = info.getProperties();
if (timedOutAt <= 0) {
String sessionTimedOutProp = sessionProperties.get("SessionTimedOut");
if (sessionTimedOutProp != null) {
try {
timedOutAt = Long.parseLong(sessionTimedOutProp);
} catch (NumberFormatException e) {
sessionDebug.error("Invalid timeout value "
+ sessionTimedOutProp, e);
}
}
}
latestRefreshTime = currentTimeMillis() / 1000;
// note : do not use getProperty() call here to avoid unexpected
// recursion via
// refresh()
String restrictionProp = sessionProperties.get(TOKEN_RESTRICTION_PROP);
if (restrictionProp != null) {
try {
setRestriction(TokenRestrictionFactory.unmarshal(restrictionProp));
} catch (Exception e) {
throw new SessionException(e);
}
}
}
/**
* Sets a token restriction on this session. Optional operation - specific sub-classes can throw an exception if
* not supported.
*
* @param restriction the restriction to apply to this session.
* @throws UnsupportedOperationException if this session type does not support token restrictions.
*/
protected void setRestriction(TokenRestriction restriction) {
this.restriction = restriction;
}
/**
* populate context object with admin token
* @exception SessionException
* @param appSSOToken application SSO Token to bet set
*/
void createContext(SSOToken appSSOToken) throws SessionException
{
if (appSSOToken == null) {
if (sessionDebug.warningEnabled()) {
sessionDebug.warning("Session."
+ "createContext():, "
+ "cannot obtain application SSO token, "
+ "defaulting to IP address");
}
} else {
sessionDebug.message("Session."
+ "createContext():, "
+ "setting context to application SSO token");
context = appSSOToken;
}
}
/**
* Set the timeout time for this Session if it wasn't already set.
*/
public void timeout() {
if (timedOutAt <= 0) {
timedOutAt = currentTimeMillis() / 1000;
}
}
/**
* Add listener to Internal Session.
*/
public void addInternalSessionListener() {
try {
if (SessionNotificationHandler.handler == null) {
SessionNotificationHandler.handler = new SessionNotificationHandler(SessionCache.getInstance());
PLLClient.addNotificationHandler(SESSION_SERVICE,
SessionNotificationHandler.handler);
}
String url = WebtopNaming.getNotificationURL().toString();
SessionOperations operations = sessionOperationStrategy.getOperation(sessionID);
operations.addSessionListener(this, url);
} catch (Exception e) {
//todo : something! :-D
}
}
/**
* Returns true if cookies are supported else false. The
* cookieSupport
value is first determined from the Session ID
* object , if that is null then it is determined based on the cookie mode
* value set in the Session object else cookieSupport
value is
* retrieved from the session property cookieSupport
. If
* cookie Support value is not determined then the the default "false" is
* assumed.
*/
public boolean getCookieSupport() {
boolean cookieSupport = false;
try {
Boolean cookieMode = sessionID.getCookieMode();
if (cookieMode != null) {
cookieSupport = cookieMode.booleanValue();
} else if (this.cookieMode != null) {
cookieSupport = this.cookieMode.booleanValue();
} else {
String cookieSupportStr = getProperty("cookieSupport");
if (cookieSupportStr != null) {
cookieSupport = cookieSupportStr.equals("true");
}
}
} catch (Exception ex) {
sessionDebug.error("Error getting cookieSupport value: ", ex);
cookieSupport = true;
}
if (sessionDebug.messageEnabled()) {
sessionDebug.message("Session: getCookieSupport: " + cookieSupport);
}
return cookieSupport;
}
/**
* Set the cookie Mode based on whether the request has cookies or not. This
* method is called from createSSOToken(request)
method in
* SSOTokenManager
.
*
* @param cookieMode whether request has cookies or not.
*/
public void setCookieMode(Boolean cookieMode) {
if (sessionDebug.messageEnabled()) {
sessionDebug.message("CookieMode is:" + cookieMode);
}
if (cookieMode != null) {
this.cookieMode = cookieMode;
}
}
public TokenRestriction getRestriction() throws SessionException {
return restriction;
}
Object getContext() {
return context;
}
/**
* Returns a stable ID that can be used as a unique identifier when storing this session.
*
* @return a unique stable storage id.
*/
@Override
public String getStableStorageID() {
return sessionID.getExtension().getStorageKey();
}
private boolean usingCachedBasedPolling() {
return SystemProperties.getAsBoolean(CACHED_BASE_POLLING_PROPERTY, false);
}
}