001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.main;
029
030import org.opencms.configuration.CmsSystemConfiguration.UserSessionMode;
031import org.opencms.db.CmsUserSettings;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsProject;
034import org.opencms.file.CmsRequestContext;
035import org.opencms.file.CmsUser;
036import org.opencms.main.CmsBroadcast.ContentMode;
037import org.opencms.security.CmsCustomLoginException;
038import org.opencms.security.CmsRole;
039import org.opencms.security.CmsSecurityException;
040import org.opencms.security.CmsUserLog;
041import org.opencms.ui.login.CmsLoginHelper;
042import org.opencms.util.CmsRequestUtil;
043import org.opencms.util.CmsStringUtil;
044import org.opencms.util.CmsUUID;
045import org.opencms.workplace.CmsWorkplace;
046import org.opencms.workplace.CmsWorkplaceManager;
047import org.opencms.workplace.tools.CmsToolManager;
048
049import java.io.PrintWriter;
050import java.io.StringWriter;
051import java.util.Collections;
052import java.util.Enumeration;
053import java.util.Iterator;
054import java.util.List;
055
056import javax.servlet.http.HttpServletRequest;
057import javax.servlet.http.HttpSession;
058import javax.servlet.http.HttpSessionEvent;
059
060import org.apache.commons.collections.Buffer;
061import org.apache.commons.collections.BufferUtils;
062import org.apache.commons.collections.buffer.CircularFifoBuffer;
063import org.apache.commons.logging.Log;
064
065/**
066 * Keeps track of the sessions running on the OpenCms server and
067 * provides a session info storage which is used to get an overview
068 * about currently authenticated OpenCms users, as well as sending broadcasts between users.<p>
069 *
070 * For each authenticated OpenCms user, a {@link org.opencms.main.CmsSessionInfo} object
071 * holds the information about the users status.<p>
072 *
073 * When a user session is invalidated, the user info will be removed.
074 * This happens when a user log out, or when his session times out.<p>
075 *
076 * <b>Please Note:</b> The current implementation does not provide any permission checking,
077 * so all users can access the methods of this manager. Permission checking
078 * based on the current users OpenCms context may be added in a future OpenCms release.<p>
079 *
080 * @since 6.0.0
081 */
082public class CmsSessionManager {
083
084    /** Header key 'true-client-ip' used by akamai proxies. */
085    public static final String HEADER_TRUE_CLIENT_IP = "true-client-ip";
086
087    /** Header key 'user-agent'. */
088    public static final String HEADER_USER_AGENT = "user-agent";
089
090    /** Request header containing the real client IP address. */
091    public static final String HEADER_X_FORWARDED_FOR = "x-forwarded-for";
092
093    /** Name of the logger for logging user switches. */
094    public static final String NAME_USERSWITCH = "userswitch";
095
096    /** Session attribute key for client token. */
097    private static final String CLIENT_TOKEN = "client-token";
098
099    /** The log object for this class. */
100    private static final Log LOG = CmsLog.getLog(CmsSessionManager.class);
101
102    /** Special logger for logging user switches. */
103    private static final Log USERSWITCH = CmsLog.getLog(NAME_USERSWITCH);
104
105    static {
106        CmsLog.makeChannelNonManageable(NAME_USERSWITCH);
107    }
108
109    /** Lock object for synchronized session count updates. */
110    private Object m_lockSessionCount;
111
112    /** Counter for the currently active sessions. */
113    private int m_sessionCountCurrent;
114
115    /** Counter for all sessions created so far. */
116    private int m_sessionCountTotal;
117
118    /** Session storage provider instance. */
119    private I_CmsSessionStorageProvider m_sessionStorageProvider;
120
121    /** The user session mode. */
122    private UserSessionMode m_userSessionMode;
123
124    /** Admin CmsObject. */
125    private CmsObject m_adminCms;
126
127    /**
128     * Creates a new instance of the OpenCms session manager.<p>
129     */
130    protected CmsSessionManager() {
131
132        // create a lock object for the session counter
133        m_lockSessionCount = new Object();
134    }
135
136    /**
137     * Checks whether a new session can be created for the user, and throws an exception if not.<p>
138     *
139     * @param user the user to check
140     * @throws CmsException if no new session for the user can't be created
141     */
142    public void checkCreateSessionForUser(CmsUser user) throws CmsException {
143
144        if (getUserSessionMode() == UserSessionMode.single) {
145            List<CmsSessionInfo> infos = getSessionInfos(user.getId());
146            if (!infos.isEmpty()) {
147                throw new CmsCustomLoginException(
148                    org.opencms.security.Messages.get().container(
149                        org.opencms.security.Messages.ERR_ALREADY_LOGGED_IN_0));
150            }
151        }
152
153    }
154
155    /**
156     * Returns the broadcast queue for the given OpenCms session id.<p>
157     *
158     * @param sessionId the OpenCms session id to get the broadcast queue for
159     *
160     * @return the broadcast queue for the given OpenCms session id
161     */
162    public Buffer getBroadcastQueue(String sessionId) {
163
164        CmsSessionInfo sessionInfo = getSessionInfo(getSessionUUID(sessionId));
165        if (sessionInfo == null) {
166            // return empty message buffer if the session is gone or not available
167            return BufferUtils.synchronizedBuffer(new CircularFifoBuffer(CmsSessionInfo.QUEUE_SIZE));
168        }
169        return sessionInfo.getBroadcastQueue();
170    }
171
172    /**
173     * Returns the number of sessions currently authenticated in the OpenCms security system.<p>
174     *
175     * @return the number of sessions currently authenticated in the OpenCms security system
176     */
177    public int getSessionCountAuthenticated() {
178
179        // since this method could be called from another thread
180        // we have to prevent access before initialization
181        if (m_sessionStorageProvider == null) {
182            return 0;
183        }
184        return m_sessionStorageProvider.getSize();
185    }
186
187    /**
188     * Returns the number of current sessions, including the sessions of not authenticated guest users.<p>
189     *
190     * @return the number of current sessions, including the sessions of not authenticated guest users
191     */
192    public int getSessionCountCurrent() {
193
194        return m_sessionCountCurrent;
195    }
196
197    /**
198     * Returns the number of total sessions generated so far, including already destroyed sessions.<p>
199     *
200     * @return the number of total sessions generated so far, including already destroyed sessions
201     */
202    public int getSessionCountTotal() {
203
204        return m_sessionCountTotal;
205    }
206
207    /**
208     * Returns the complete user session info of a user from the session storage,
209     * or <code>null</code> if this session id has no session info attached.<p>
210     *
211     * @param sessionId the OpenCms session id to return the session info for
212     *
213     * @return the complete user session info of a user from the session storage
214     */
215    public CmsSessionInfo getSessionInfo(CmsUUID sessionId) {
216
217        // since this method could be called from another thread
218        // we have to prevent access before initialization
219        if (m_sessionStorageProvider == null) {
220            return null;
221        }
222        return m_sessionStorageProvider.get(sessionId);
223    }
224
225    /**
226     * Returns the OpenCms user session info for the given request,
227     * or <code>null</code> if no user session is available.<p>
228     *
229     * @param req the current request
230     *
231     * @return the OpenCms user session info for the given request, or <code>null</code> if no user session is available
232     */
233    public CmsSessionInfo getSessionInfo(HttpServletRequest req) {
234
235        HttpSession session = req.getSession(false);
236        if (session == null) {
237            // special case for accessing a session from "outside" requests (e.g. upload applet)
238            String sessionId = req.getHeader(CmsRequestUtil.HEADER_JSESSIONID);
239            return sessionId == null ? null : getSessionInfo(sessionId);
240        }
241        return getSessionInfo(session);
242    }
243
244    /**
245     * Returns the OpenCms user session info for the given http session,
246     * or <code>null</code> if no user session is available.<p>
247     *
248     * @param session the current http session
249     *
250     * @return the OpenCms user session info for the given http session, or <code>null</code> if no user session is available
251     */
252    public CmsSessionInfo getSessionInfo(HttpSession session) {
253
254        if (session == null) {
255            return null;
256        }
257        CmsUUID sessionId = (CmsUUID)session.getAttribute(CmsSessionInfo.ATTRIBUTE_SESSION_ID);
258        return (sessionId == null) ? null : getSessionInfo(sessionId);
259    }
260
261    /**
262     * Returns the complete user session info of a user from the session storage,
263     * or <code>null</code> if this session id has no session info attached.<p>
264     *
265     * @param sessionId the OpenCms session id to return the session info for,
266     * this must be a String representation of a {@link CmsUUID}
267     *
268     * @return the complete user session info of a user from the session storage
269     *
270     * @see #getSessionInfo(CmsUUID)
271     */
272    public CmsSessionInfo getSessionInfo(String sessionId) {
273
274        return getSessionInfo(getSessionUUID(sessionId));
275    }
276
277    /**
278     * Returns all current session info objects.<p>
279     *
280     * @return all current session info objects
281     */
282    public List<CmsSessionInfo> getSessionInfos() {
283
284        // since this method could be called from another thread
285        // we have to prevent access before initialization
286        if (m_sessionStorageProvider == null) {
287            return Collections.emptyList();
288        }
289        return m_sessionStorageProvider.getAll();
290    }
291
292    /**
293     * Returns a list of all active session info objects for the specified user.<p>
294     *
295     * An OpenCms user can have many active sessions.
296     * This is e.g. possible when two people have logged in to the system using the
297     * same username. Even one person can have multiple sessions if he
298     * is logged in to OpenCms with several browser windows at the same time.<p>
299     *
300     * @param userId the id of the user
301     *
302     * @return a list of all active session info objects for the specified user
303     */
304    public List<CmsSessionInfo> getSessionInfos(CmsUUID userId) {
305
306        // since this method could be called from another thread
307        // we have to prevent access before initialization
308        if (m_sessionStorageProvider == null) {
309            return Collections.emptyList();
310        }
311        return m_sessionStorageProvider.getAllOfUser(userId);
312    }
313
314    /**
315     * Gets the user session mode.<p>
316     *
317     * @return the user session mode
318     */
319    public UserSessionMode getUserSessionMode() {
320
321        return m_userSessionMode;
322    }
323
324    /**
325     * Returns whether the current request has a valid client token.<p>
326     * Used to prevent session hijacking.<p>
327     *
328     * @param req the current request
329     *
330     * @return <code>true</code> in case the request has a valid token
331     */
332    public boolean hasValidClientToken(HttpServletRequest req) {
333
334        String requestToken = generateClientToken(req);
335        String sessionToken = null;
336        HttpSession session = req.getSession(false);
337        if (session != null) {
338            sessionToken = (String)session.getAttribute(CLIENT_TOKEN);
339        }
340        return requestToken.equals(sessionToken);
341    }
342
343    /**
344     * Kills all sessions for the given user.<p>
345     *
346     * @param cms the current CMS context
347     * @param user the user for whom the sessions should be killed
348     *
349     * @throws CmsException if something goes wrong
350     */
351    public void killSession(CmsObject cms, CmsUser user) throws CmsException {
352
353        OpenCms.getRoleManager().checkRole(cms, CmsRole.ACCOUNT_MANAGER);
354        List<CmsSessionInfo> infos = getSessionInfos(user.getId());
355        for (CmsSessionInfo info : infos) {
356            m_sessionStorageProvider.remove(info.getSessionId());
357        }
358    }
359
360    /**
361     * Destroys a session given the session id. Only allowed for users which have the "account manager" role.<p>
362     *
363     * @param cms the current CMS context
364     * @param sessionid the session id
365     *
366     * @throws CmsException if something goes wrong
367     */
368    public void killSession(CmsObject cms, CmsUUID sessionid) throws CmsException {
369
370        OpenCms.getRoleManager().checkRole(cms, CmsRole.ACCOUNT_MANAGER);
371        m_sessionStorageProvider.remove(sessionid);
372
373    }
374
375    /**
376     * Sends a broadcast to all sessions of all currently authenticated users.<p>
377     *
378     * @param cms the OpenCms user context of the user sending the broadcast
379     *
380     * @param message the message to broadcast
381     */
382    @Deprecated
383    public void sendBroadcast(CmsObject cms, String message) {
384
385        sendBroadcast(cms, message, ContentMode.plain);
386    }
387
388    /**
389     * Sends a broadcast to all sessions of all currently authenticated users.<p>
390     *
391     * @param cms the OpenCms user context of the user sending the broadcast
392     *
393     * @param message the message to broadcast
394     * @param repeat repeat this message
395     */
396    @Deprecated
397    public void sendBroadcast(CmsObject cms, String message, boolean repeat) {
398
399        sendBroadcast(cms, message, repeat, ContentMode.plain);
400
401    }
402
403    /**
404     * Sends a broadcast to all sessions of all currently authenticated users.<p>
405     *
406     * @param cms the OpenCms user context of the user sending the broadcast
407     * @param message the message to broadcast
408     * @param repeat repeat this message
409     * @param mode the content mode to use
410     */
411    public void sendBroadcast(CmsObject cms, String message, boolean repeat, ContentMode mode) {
412
413        if (CmsStringUtil.isEmptyOrWhitespaceOnly(message)) {
414            // don't broadcast empty messages
415            return;
416        }
417        // create the broadcast
418        CmsBroadcast broadcast = new CmsBroadcast(cms.getRequestContext().getCurrentUser(), message, repeat, mode);
419        // send the broadcast to all authenticated sessions
420        Iterator<CmsSessionInfo> i = m_sessionStorageProvider.getAll().iterator();
421        while (i.hasNext()) {
422            CmsSessionInfo sessionInfo = i.next();
423            if (m_sessionStorageProvider.get(sessionInfo.getSessionId()) != null) {
424                // double check for concurrent modification
425                sessionInfo.getBroadcastQueue().add(broadcast);
426            }
427        }
428
429    }
430
431    /**
432     * Sends a broadcast to all sessions of all currently authenticated users.<p>
433     *
434     * @param cms the OpenCms user context of the user sending the broadcast
435     * @param message the message to broadcast
436     * @param mode the content mode
437     */
438    public void sendBroadcast(CmsObject cms, String message, ContentMode mode) {
439
440        sendBroadcast(cms, message, false, mode);
441    }
442
443    /**
444     * Sends a broadcast to the specified user session.<p>
445     *
446     * @param cms the OpenCms user context of the user sending the broadcast
447     *
448     * @param message the message to broadcast
449     * @param sessionId the OpenCms session uuid target (receiver) of the broadcast
450     */
451    @Deprecated
452    public void sendBroadcast(CmsObject cms, String message, String sessionId) {
453
454        sendBroadcast(cms, message, sessionId, false);
455
456    }
457
458    /**
459     * Sends a broadcast to the specified user session.<p>
460     *
461     * @param cms the OpenCms user context of the user sending the broadcast
462     *
463     * @param message the message to broadcast
464     * @param sessionId the OpenCms session uuid target (receiver) of the broadcast
465     * @param repeat repeat this message
466     */
467    @Deprecated
468    public void sendBroadcast(CmsObject cms, String message, String sessionId, boolean repeat) {
469
470        sendBroadcast(cms, message, sessionId, repeat, ContentMode.plain);
471
472    }
473
474    /**
475     * Sends a broadcast to the specified user session.<p>
476     *
477     * @param cms the OpenCms user context of the user sending the broadcast
478     *
479     * @param message the message to broadcast
480     * @param sessionId the OpenCms session uuid target (receiver) of the broadcast
481     * @param repeat repeat this message
482     * @param mode the content mode to use
483     */
484    public void sendBroadcast(CmsObject cms, String message, String sessionId, boolean repeat, ContentMode mode) {
485
486        if (CmsStringUtil.isEmptyOrWhitespaceOnly(message)) {
487            // don't broadcast empty messages
488            return;
489        }
490        // send the broadcast only to the selected session
491        CmsSessionInfo sessionInfo = m_sessionStorageProvider.get(new CmsUUID(sessionId));
492        if (sessionInfo != null) {
493            // double check for concurrent modification
494            sessionInfo.getBroadcastQueue().add(
495                new CmsBroadcast(cms.getRequestContext().getCurrentUser(), message, repeat, mode));
496        }
497    }
498
499    /**
500     * Sends a broadcast to the specified user session.<p>
501     *
502     * @param cms the OpenCms user context of the user sending the broadcast
503     *
504     * @param message the message to broadcast
505     * @param sessionId the OpenCms session uuid target (receiver) of the broadcast
506     * @param mode the content mode to use
507     */
508    public void sendBroadcast(CmsObject cms, String message, String sessionId, ContentMode mode) {
509
510        sendBroadcast(cms, message, sessionId, false, mode);
511    }
512
513    /**
514     * Sends a broadcast to all sessions of a given user.<p>
515     *
516     * The user sending the message may be a real user like
517     * <code>cms.getRequestContext().currentUser()</code> or
518     * <code>null</code> for a system message.<p>
519     *
520     * @param fromUser the user sending the broadcast
521     * @param message the message to broadcast
522     * @param toUser the target (receiver) of the broadcast
523     */
524    @Deprecated
525    public void sendBroadcast(CmsUser fromUser, String message, CmsUser toUser) {
526
527        sendBroadcast(fromUser, message, toUser, ContentMode.plain);
528
529    }
530
531    /**
532     * Sends a broadcast to all sessions of a given user.<p>
533     *
534     * The user sending the message may be a real user like
535     * <code>cms.getRequestContext().currentUser()</code> or
536     * <code>null</code> for a system message.<p>
537     *
538     * @param fromUser the user sending the broadcast
539     * @param message the message to broadcast
540     * @param toUser the target (receiver) of the broadcast
541     * @param mode the content mode to use
542     */
543    public void sendBroadcast(CmsUser fromUser, String message, CmsUser toUser, CmsBroadcast.ContentMode mode) {
544
545        if (CmsStringUtil.isEmptyOrWhitespaceOnly(message)) {
546            // don't broadcast empty messages
547            return;
548        }
549        // create the broadcast
550        CmsBroadcast broadcast = new CmsBroadcast(fromUser, message, mode);
551        List<CmsSessionInfo> userSessions = getSessionInfos(toUser.getId());
552        Iterator<CmsSessionInfo> i = userSessions.iterator();
553        // send the broadcast to all sessions of the selected user
554        while (i.hasNext()) {
555            CmsSessionInfo sessionInfo = i.next();
556            if (m_sessionStorageProvider.get(sessionInfo.getSessionId()) != null) {
557                // double check for concurrent modification
558                sessionInfo.getBroadcastQueue().add(broadcast);
559            }
560        }
561
562    }
563
564    /**
565     * Switches the current user to the given user. The session info is rebuild as if the given user
566     * performs a login at the workplace.
567     *
568     * @param cms the current CmsObject
569     * @param req the current request
570     * @param user the user to switch to
571     *
572     * @return the direct edit target if available
573     *
574     * @throws CmsException if something goes wrong
575     */
576    public String switchUser(CmsObject cms, HttpServletRequest req, CmsUser user) throws CmsException {
577
578        return switchUserFromSession(cms, req, user, null);
579    }
580
581    /**
582     * Switches the current user to the given user. The session info is rebuild as if the given user
583     * performs a login at the workplace.
584     *
585     * @param cms the current CmsObject
586     * @param req the current request
587     * @param user the user to switch to
588     * @param sessionInfo to switch to a currently logged in user using the same session state
589     *
590     * @return the direct edit target if available
591     *
592     * @throws CmsException if something goes wrong
593     */
594    public String switchUserFromSession(CmsObject cms, HttpServletRequest req, CmsUser user, CmsSessionInfo sessionInfo)
595    throws CmsException {
596
597        // only user with root administrator role are allowed to switch the user
598        OpenCms.getRoleManager().checkRole(cms, CmsRole.ADMINISTRATOR.forOrgUnit(user.getOuFqn()));
599        CmsSessionInfo info = getSessionInfo(req);
600        HttpSession session = req.getSession(false);
601        if ((info == null) || (session == null)) {
602            throw new CmsException(Messages.get().container(Messages.ERR_NO_SESSIONINFO_SESSION_0));
603        }
604
605        if (!OpenCms.getRoleManager().hasRole(cms, user.getName(), CmsRole.ELEMENT_AUTHOR)) {
606            throw new CmsSecurityException(Messages.get().container(Messages.ERR_NO_WORKPLACE_PERMISSIONS_0));
607        }
608        String oldUser = cms.getRequestContext().getCurrentUser().getName();
609
610        // get the user settings for the given user and set the start project and the site root
611        CmsUserSettings settings = new CmsUserSettings(user);
612        String ouFqn = user.getOuFqn();
613
614        CmsProject userProject;
615        String userSiteRoot;
616
617        if (sessionInfo == null) {
618            userProject = cms.readProject(
619                ouFqn + OpenCms.getWorkplaceManager().getDefaultUserSettings().getStartProject());
620            try {
621                userProject = cms.readProject(settings.getStartProject());
622            } catch (Exception e) {
623                // ignore, use default
624            }
625            CmsObject userCms = OpenCms.initCmsObject(m_adminCms, new CmsContextInfo(user.getName()));
626
627            userSiteRoot = CmsWorkplace.getStartSiteRoot(userCms, settings);
628        } else {
629            userProject = cms.readProject(sessionInfo.getProject());
630            userSiteRoot = sessionInfo.getSiteRoot();
631        }
632
633        CmsRequestContext context = new CmsRequestContext(
634            user,
635            userProject,
636            null,
637            cms.getRequestContext().getRequestMatcher(),
638            userSiteRoot,
639            cms.getRequestContext().isSecureRequest(),
640            null,
641            null,
642            null,
643            0,
644            null,
645            null,
646            ouFqn,
647            false);
648        // delete the stored workplace settings, so the session has to receive them again
649        session.removeAttribute(CmsWorkplaceManager.SESSION_WORKPLACE_SETTINGS);
650
651        // create a new CmsSessionInfo and store it inside the session map
652        CmsSessionInfo newInfo = new CmsSessionInfo(context, info.getSessionId(), info.getMaxInactiveInterval());
653        addSessionInfo(newInfo);
654        // set the site root, project and ou fqn to current cms context
655        cms.getRequestContext().setSiteRoot(userSiteRoot);
656        cms.getRequestContext().setCurrentProject(userProject);
657        cms.getRequestContext().setOuFqn(user.getOuFqn());
658        USERSWITCH.info("User '" + oldUser + "' switched to user '" + user.getName() + "'");
659        CmsUserLog.logSwitchUser(cms, user.getName());
660        String directEditTarget = CmsLoginHelper.getDirectEditPath(cms, new CmsUserSettings(user), false);
661        return directEditTarget != null
662        ? OpenCms.getLinkManager().substituteLink(cms, directEditTarget, userSiteRoot)
663        : null;
664    }
665
666    /**
667     * @see java.lang.Object#toString()
668     */
669    @Override
670    public String toString() {
671
672        StringBuffer output = new StringBuffer();
673        Iterator<CmsSessionInfo> i = m_sessionStorageProvider.getAll().iterator();
674        output.append("[CmsSessions]:\n");
675        while (i.hasNext()) {
676            CmsSessionInfo sessionInfo = i.next();
677            output.append(sessionInfo.getSessionId().toString());
678            output.append(" : ");
679            output.append(sessionInfo.getUserId().toString());
680            output.append('\n');
681        }
682        return output.toString();
683    }
684
685    /**
686     * Updates the the OpenCms session data used for quick authentication of users.<p>
687     *
688     * This is required if the user data (current group or project) was changed in
689     * the requested document.<p>
690     *
691     * The user data is only updated if the user was authenticated to the system.
692     *
693     * @param cms the current OpenCms user context
694     * @param req the current request
695     */
696    public void updateSessionInfo(CmsObject cms, HttpServletRequest req) {
697
698        updateSessionInfo(cms, req, false);
699    }
700
701    /**
702     * Updates the the OpenCms session data used for quick authentication of users.<p>
703     *
704     * This is required if the user data (current group or project) was changed in
705     * the requested document.<p>
706     *
707     * The user data is only updated if the user was authenticated to the system.
708     *
709     * @param cms the current OpenCms user context
710     * @param req the current request
711     * @param isHeartBeatRequest in case of heart beat requests
712     */
713    public void updateSessionInfo(CmsObject cms, HttpServletRequest req, boolean isHeartBeatRequest) {
714
715        if (!cms.getRequestContext().isUpdateSessionEnabled()) {
716            // this request must not update the user session info
717            // this is true for long running "thread" requests, e.g. during project publish
718            return;
719        }
720
721        if (cms.getRequestContext().getUri().equals(CmsToolManager.VIEW_JSPPAGE_LOCATION)) {
722            // this request must not update the user session info
723            // if not the switch user feature would not work
724            return;
725        }
726
727        if (!cms.getRequestContext().getCurrentUser().isGuestUser()) {
728            // Guest user requests don't need to update the OpenCms user session information
729
730            // get the session info object for the user
731            CmsSessionInfo sessionInfo = getSessionInfo(req);
732            if (sessionInfo != null) {
733                // update the users session information
734                sessionInfo.update(cms.getRequestContext(), isHeartBeatRequest);
735                addSessionInfo(sessionInfo);
736            } else {
737                HttpSession session = req.getSession(false);
738                // only create session info if a session is already available
739                if (session != null) {
740                    // create a new session info for the user
741                    sessionInfo = new CmsSessionInfo(
742                        cms.getRequestContext(),
743                        new CmsUUID(),
744                        session.getMaxInactiveInterval());
745                    // append the session info to the http session
746                    session.setAttribute(CmsSessionInfo.ATTRIBUTE_SESSION_ID, sessionInfo.getSessionId().clone());
747                    // store a client token to prevent session hijacking
748                    session.setAttribute(CLIENT_TOKEN, generateClientToken(req));
749                    // update the session info user data
750                    addSessionInfo(sessionInfo);
751                }
752            }
753        }
754    }
755
756    /**
757     * Updates the the OpenCms session data used for quick authentication of users.<p>
758     *
759     * This is required if the user data (current group or project) was changed in
760     * the requested document.<p>
761     *
762     * The user data is only updated if the user was authenticated to the system.
763     *
764     * @param cms the current OpenCms user context
765     * @param session the current session
766     */
767    public void updateSessionInfo(CmsObject cms, HttpSession session) {
768
769        if (session == null) {
770            return;
771        }
772
773        if (!cms.getRequestContext().isUpdateSessionEnabled()) {
774            // this request must not update the user session info
775            // this is true for long running "thread" requests, e.g. during project publish
776            return;
777        }
778
779        if (cms.getRequestContext().getUri().equals(CmsToolManager.VIEW_JSPPAGE_LOCATION)) {
780            // this request must not update the user session info
781            // if not the switch user feature would not work
782            return;
783        }
784
785        if (!cms.getRequestContext().getCurrentUser().isGuestUser()) {
786            // Guest user requests don't need to update the OpenCms user session information
787
788            // get the session info object for the user
789            CmsSessionInfo sessionInfo = getSessionInfo(session);
790            if (sessionInfo != null) {
791                // update the users session information
792                sessionInfo.update(cms.getRequestContext());
793                addSessionInfo(sessionInfo);
794            } else {
795                sessionInfo = new CmsSessionInfo(
796                    cms.getRequestContext(),
797                    new CmsUUID(),
798                    session.getMaxInactiveInterval());
799                // append the session info to the http session
800                session.setAttribute(CmsSessionInfo.ATTRIBUTE_SESSION_ID, sessionInfo.getSessionId().clone());
801                // update the session info user data
802                addSessionInfo(sessionInfo);
803            }
804        }
805    }
806
807    /**
808     * Updates all session info objects, so that invalid projects
809     * are replaced by the Online project.<p>
810     *
811     * @param cms the cms context
812     */
813    public void updateSessionInfos(CmsObject cms) {
814
815        // get all sessions
816        List<CmsSessionInfo> userSessions = getSessionInfos();
817        Iterator<CmsSessionInfo> i = userSessions.iterator();
818        while (i.hasNext()) {
819            CmsSessionInfo sessionInfo = i.next();
820            // check is the project stored in this session is not existing anymore
821            // if so, set it to the online project
822            CmsUUID projectId = sessionInfo.getProject();
823            try {
824                cms.readProject(projectId);
825            } catch (CmsException e) {
826                // the project does not longer exist, update the project information with the online project
827                sessionInfo.setProject(CmsProject.ONLINE_PROJECT_ID);
828                addSessionInfo(sessionInfo);
829            }
830        }
831    }
832
833    /**
834     * Adds a new session info into the session storage.<p>
835     *
836     * @param sessionInfo the session info to store for the id
837     */
838    protected void addSessionInfo(CmsSessionInfo sessionInfo) {
839
840        if (getUserSessionMode() == UserSessionMode.standard) {
841            m_sessionStorageProvider.put(sessionInfo);
842        } else if (getUserSessionMode() == UserSessionMode.single) {
843            CmsUUID userId = sessionInfo.getUserId();
844            List<CmsSessionInfo> infos = getSessionInfos(userId);
845            if (infos.isEmpty()
846                || ((infos.size() == 1) && infos.get(0).getSessionId().equals(sessionInfo.getSessionId()))) {
847                m_sessionStorageProvider.put(sessionInfo);
848            } else {
849                throw new RuntimeException("Can't create another session for the same user.");
850            }
851        }
852    }
853
854    /**
855     * Returns the UUID representation for the given session id String.<p>
856     *
857     * @param sessionId the session id String to return the  UUID representation for
858     *
859     * @return the UUID representation for the given session id String
860     */
861    protected CmsUUID getSessionUUID(String sessionId) {
862
863        return new CmsUUID(sessionId);
864    }
865
866    /**
867     * Sets the storage provider.<p>
868     *
869     * @param sessionStorageProvider the storage provider implementation
870     * @param adminCms
871     */
872    protected void initialize(I_CmsSessionStorageProvider sessionStorageProvider, CmsObject adminCms) {
873
874        m_sessionStorageProvider = sessionStorageProvider;
875        m_sessionStorageProvider.initialize();
876        m_adminCms = adminCms;
877    }
878
879    /**
880     * Called by the {@link OpenCmsListener} when a http session is created.<p>
881     *
882     * @param event the http session event
883     *
884     * @see javax.servlet.http.HttpSessionListener#sessionCreated(javax.servlet.http.HttpSessionEvent)
885     * @see OpenCmsListener#sessionCreated(HttpSessionEvent)
886     */
887    protected void sessionCreated(HttpSessionEvent event) {
888
889        HttpServletRequest request = OpenCmsServlet.currentRequestStack.top();
890        String tid = "[" + Thread.currentThread().getId() + "] ";
891        synchronized (m_lockSessionCount) {
892            m_sessionCountCurrent = (m_sessionCountCurrent <= 0) ? 1 : (m_sessionCountCurrent + 1);
893            m_sessionCountTotal++;
894            if (LOG.isInfoEnabled()) {
895                LOG.info(
896                    tid
897                        + Messages.get().getBundle().key(
898                            Messages.LOG_SESSION_CREATED_2,
899                            new Integer(m_sessionCountTotal),
900                            new Integer(m_sessionCountCurrent)));
901            }
902        }
903
904        if (LOG.isDebugEnabled()) {
905            LOG.debug(tid + Messages.get().getBundle().key(Messages.LOG_SESSION_CREATED_1, event.getSession().getId()));
906            if (request != null) {
907                LOG.debug(tid + "Session created in request: " + request.getRequestURL());
908            }
909            StringWriter sw = new StringWriter();
910            new Throwable("").printStackTrace(new PrintWriter(sw));
911            String stackTrace = sw.toString();
912            LOG.debug(tid + "Stack = \n" + stackTrace);
913        }
914    }
915
916    /**
917     * Called by the {@link OpenCmsListener} when a http session is destroyed.<p>
918     *
919     * @param event the http session event
920     *
921     * @see javax.servlet.http.HttpSessionListener#sessionDestroyed(javax.servlet.http.HttpSessionEvent)
922     * @see OpenCmsListener#sessionDestroyed(HttpSessionEvent)
923     */
924    protected void sessionDestroyed(HttpSessionEvent event) {
925
926        synchronized (m_lockSessionCount) {
927            m_sessionCountCurrent = (m_sessionCountCurrent <= 0) ? 0 : (m_sessionCountCurrent - 1);
928            if (LOG.isInfoEnabled()) {
929                LOG.info(
930                    Messages.get().getBundle().key(
931                        Messages.LOG_SESSION_DESTROYED_2,
932                        new Integer(m_sessionCountTotal),
933                        new Integer(m_sessionCountCurrent)));
934            }
935        }
936
937        CmsSessionInfo sessionInfo = getSessionInfo(event.getSession());
938        CmsUUID userId = null;
939        if (sessionInfo != null) {
940            userId = sessionInfo.getUserId();
941            m_sessionStorageProvider.remove(sessionInfo.getSessionId());
942        }
943
944        if ((userId != null) && (getSessionInfos(userId).size() == 0)) {
945            // remove the temporary locks of this user from memory
946            OpenCmsCore.getInstance().getLockManager().removeTempLocks(userId);
947        }
948
949        HttpSession session = event.getSession();
950        Enumeration<?> attrNames = session.getAttributeNames();
951        while (attrNames.hasMoreElements()) {
952            String attrName = (String)attrNames.nextElement();
953            Object attribute = session.getAttribute(attrName);
954            if (attribute instanceof I_CmsSessionDestroyHandler) {
955                try {
956                    ((I_CmsSessionDestroyHandler)attribute).onSessionDestroyed();
957                } catch (Exception e) {
958                    LOG.error(e.getLocalizedMessage(), e);
959                }
960            }
961        }
962
963        if (LOG.isDebugEnabled()) {
964            LOG.debug(Messages.get().getBundle().key(Messages.LOG_SESSION_DESTROYED_1, event.getSession().getId()));
965        }
966    }
967
968    /**
969     * Sets the user session mode.<p>
970     *
971     * @param userSessionMode the user session mode
972     */
973    protected void setUserSessionMode(UserSessionMode userSessionMode) {
974
975        m_userSessionMode = userSessionMode;
976    }
977
978    /**
979     * Removes all stored session info objects.<p>
980     *
981     * @throws Exception if something goes wrong
982     */
983    protected void shutdown() throws Exception {
984
985        if (m_sessionStorageProvider != null) {
986            m_sessionStorageProvider.shutdown();
987        }
988    }
989
990    /**
991     * Validates the sessions stored in this manager and removes
992     * any sessions that have become invalidated.<p>
993     */
994    protected void validateSessionInfos() {
995
996        // since this method could be called from another thread
997        // we have to prevent access before initialization
998        if (m_sessionStorageProvider == null) {
999            return;
1000        }
1001        m_sessionStorageProvider.validate();
1002    }
1003
1004    /**
1005     * Generates a token based on hashed client ip and user agent.<p>
1006     * Used to prevent session hijacking.<p>
1007     *
1008     * @param request the current request
1009     *
1010     * @return the client token
1011     */
1012    private String generateClientToken(HttpServletRequest request) {
1013
1014        String ip = request.getHeader(HEADER_TRUE_CLIENT_IP);
1015        if (CmsStringUtil.isEmptyOrWhitespaceOnly(ip)) {
1016            ip = request.getHeader(HEADER_X_FORWARDED_FOR);
1017            if ((ip != null) && ip.contains(",")) {
1018                ip = ip.split(",")[0];
1019            }
1020        }
1021        if ((ip == null) || CmsStringUtil.isEmptyOrWhitespaceOnly(ip)) {
1022            ip = request.getRemoteAddr();
1023        }
1024        return String.valueOf(ip.hashCode());
1025    }
1026}