    001/*
    002    Licensed to the Apache Software Foundation (ASF) under one
    003    or more contributor license agreements.  See the NOTICE file
    004    distributed with this work for additional information
    005    regarding copyright ownership.  The ASF licenses this file
    006    to you under the Apache License, Version 2.0 (the
    007    "License"); you may not use this file except in compliance
    008    with the License.  You may obtain a copy of the License at
    009
    010       http://www.apache.org/licenses/LICENSE-2.0
    011
    012    Unless required by applicable law or agreed to in writing,
    013    software distributed under the License is distributed on an
    014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015    KIND, either express or implied.  See the License for the
    016    specific language governing permissions and limitations
    017    under the License.
    018 */
    019package org.apache.wiki.auth;
    020
    021import org.apache.logging.log4j.LogManager;
    022import org.apache.logging.log4j.Logger;
    023import org.apache.wiki.api.core.Engine;
    024import org.apache.wiki.api.core.Session;
    025import org.apache.wiki.api.spi.Wiki;
    026import org.apache.wiki.event.WikiEventListener;
    027import org.apache.wiki.event.WikiEventManager;
    028import org.apache.wiki.event.WikiSecurityEvent;
    029import org.apache.wiki.util.comparators.PrincipalComparator;
    030
    031import javax.servlet.http.HttpServletRequest;
    032import javax.servlet.http.HttpSession;
    033import javax.servlet.http.HttpSessionEvent;
    034import javax.servlet.http.HttpSessionListener;
    035import java.security.Principal;
    036import java.util.Arrays;
    037import java.util.Collection;
    038import java.util.Map;
    039import java.util.WeakHashMap;
    040import java.util.concurrent.ConcurrentHashMap;
    041import java.util.stream.Collectors;
    042
    043/**
    044 *  <p>Manages Sessions for different Engines.</p>
    045 *  <p>The Sessions are stored both in the remote user HttpSession and in the SessionMonitor for the Engine.
    046 *  This class must be configured as a session listener in the web.xml for the wiki web application.</p>
    047 */
    048public class SessionMonitor implements HttpSessionListener {
    049
    050    private static final Logger LOG = LogManager.getLogger( SessionMonitor.class );
    051
    052    /** Map with Engines as keys, and SessionMonitors as values. */
    053    private static final ConcurrentHashMap< Engine, SessionMonitor > c_monitors = new ConcurrentHashMap<>();
    054
    055    /** Weak hashmap with HttpSessions as keys, and WikiSessions as values. */
    056    private final Map< String, Session > m_sessions = new WeakHashMap<>();
    057
    058    private Engine m_engine;
    059
    060    private final PrincipalComparator m_comparator = new PrincipalComparator();
    061
    062    /**
    063     * Returns the instance of the SessionMonitor for this wiki. Only one SessionMonitor exists per Engine.
    064     *
    065     * @param engine the wiki engine
    066     * @return the session monitor
    067     */
    068    public static SessionMonitor getInstance( final Engine engine ) {
    069        if( engine == null ) {
    070            throw new IllegalArgumentException( "Engine cannot be null." );
    071        }
    072        SessionMonitor monitor = c_monitors.get( engine );
    073        if( monitor == null ) {
    074            monitor = new SessionMonitor( engine );
    075            c_monitors.put( engine, monitor );
    076        }
    077
    078        return monitor;
    079    }
    080
    081    /** Construct the SessionListener */
    082    public SessionMonitor() {
    083    }
    084
    085    private SessionMonitor( final Engine engine ) {
    086        m_engine = engine;
    087    }
    088
    089    /**
    090     *  Just looks for a WikiSession; does not create a new one.
    091     * This method may return <code>null</code>, <em>and
    092     * callers should check for this value</em>.
    093     *
    094     *  @param session the user's HTTP session
    095     *  @return the WikiSession, if found
    096     */
    097    private Session findSession( final HttpSession session ) {
    098        final String sid = ( session == null ) ? "(null)" : session.getId();
    099        return findSession( sid );
    100    }
    101
    102    /**
    103     *  Just looks for a WikiSession; does not create a new one.
    104     * This method may return <code>null</code>, <em>and
    105     * callers should check for this value</em>.
    106     *
    107     *  @param sessionId the user's HTTP session id
    108     *  @return the WikiSession, if found
    109     */
    110    private Session findSession( final String sessionId ) {
    111        Session wikiSession = null;
    112        final String sid = ( sessionId == null ) ? "(null)" : sessionId;
    113        final Session storedSession = m_sessions.get( sid );
    114
    115        // If the weak reference returns a wiki session, return it
    116        if( storedSession != null ) {
    117            LOG.debug( "Looking up WikiSession for session ID={}... found it", sid );
    118            wikiSession = storedSession;
    119        }
    120
    121        return wikiSession;
    122    }
    123
    124    /**
    125     * <p>Looks up the wiki session associated with a user's Http session and adds it to the session cache. This method will return the
    126     * "guest session" as constructed by {@link org.apache.wiki.api.spi.SessionSPI#guest(Engine)} if the HttpSession is not currently
    127     * associated with a WikiSession. This method is guaranteed to return a non-<code>null</code> WikiSession.</p>
    128     * <p>Internally, the session is stored in a HashMap; keys are the HttpSession objects, while the values are
    129     * {@link java.lang.ref.WeakReference}-wrapped WikiSessions.</p>
    130     *
    131     * @param session the HTTP session
    132     * @return the wiki session
    133     */
    134    public final Session find( final HttpSession session ) {
    135        final Session wikiSession = findSession( session );
    136        final String sid = ( session == null ) ? "(null)" : session.getId();
    137        if( wikiSession == null ) {
    138            return createGuestSessionFor( sid );
    139        }
    140
    141        return wikiSession;
    142    }
    143
    144    /**
    145     * <p>Looks up the wiki session associated with a user's Http session and adds it to the session cache. This method will return the
    146     * "guest session" as constructed by {@link org.apache.wiki.api.spi.SessionSPI#guest(Engine)} if the HttpSession is not currently
    147     * associated with a WikiSession. This method is guaranteed to return a non-<code>null</code> WikiSession.</p>
    148     * <p>Internally, the session is stored in a HashMap; keys are the HttpSession objects, while the values are
    149     * {@link java.lang.ref.WeakReference}-wrapped WikiSessions.</p>
    150     *
    151     * @param sessionId the HTTP session
    152     * @return the wiki session
    153     */
    154    public final Session find( final String sessionId ) {
    155        final Session wikiSession = findSession( sessionId );
    156        if( wikiSession == null ) {
    157            return createGuestSessionFor( sessionId );
    158        }
    159
    160        return wikiSession;
    161    }
    162
    163    /**
    164     * Creates a new session and stashes it
    165     *
    166     * @param sessionId id looked for before creating the guest session
    167     * @return a new guest session
    168     */
    169    private Session createGuestSessionFor( final String sessionId ) {
    170        LOG.debug( "Session for session ID={}... not found. Creating guestSession()", sessionId );
    171        final Session wikiSession = Wiki.session().guest( m_engine );
    172        synchronized( m_sessions ) {
    173            m_sessions.put( sessionId, wikiSession );
    174        }
    175        return wikiSession;
    176    }
    177
    178    /**
    179     * Removes the wiki session associated with the user's HttpRequest from the session cache.
    180     *
    181     * @param request the user's HTTP request
    182     */
    183    public final void remove( final HttpServletRequest request ) {
    184        if( request == null ) {
    185            throw new IllegalArgumentException( "Request cannot be null." );
    186        }
    187        remove( request.getSession() );
    188    }
    189
    190    /**
    191     * Removes the wiki session associated with the user's HttpSession from the session cache.
    192     *
    193     * @param session the user's HTTP session
    194     */
    195    public final void remove( final HttpSession session ) {
    196        if( session == null ) {
    197            throw new IllegalArgumentException( "Session cannot be null." );
    198        }
    199        synchronized( m_sessions ) {
    200            m_sessions.remove( session.getId() );
    201        }
    202    }
    203
    204    /**
    205     * Returns the current number of active wiki sessions.
    206     * @return the number of sessions
    207     */
    208    public final int sessions()
    209    {
    210        return userPrincipals().length;
    211    }
    212
    213    /**
    214     * <p>Returns the current wiki users as a sorted array of Principal objects. The principals are those returned by
    215     * each WikiSession's {@link Session#getUserPrincipal()}'s method.</p>
    216     * <p>To obtain the list of current WikiSessions, we iterate through our session Map and obtain the list of values,
    217     * which are WikiSessions wrapped in {@link java.lang.ref.WeakReference} objects. Those <code>WeakReference</code>s
    218     * whose <code>get()</code> method returns non-<code>null</code> values are valid sessions.</p>
    219     *
    220     * @return the array of user principals
    221     */
    222    public final Principal[] userPrincipals() {
    223        final Collection<Principal> principals;
    224        synchronized ( m_sessions ) {
    225            principals = m_sessions.values().stream().map(Session::getUserPrincipal).collect(Collectors.toList());
    226        }
    227        final Principal[] p = principals.toArray( new Principal[0] );
    228        Arrays.sort( p, m_comparator );
    229        return p;
    230    }
    231
    232    /**
    233     * Registers a WikiEventListener with this instance.
    234     *
    235     * @param listener the event listener
    236     * @since 2.4.75
    237     */
    238    public final synchronized void addWikiEventListener( final WikiEventListener listener ) {
    239        WikiEventManager.addWikiEventListener( this, listener );
    240    }
    241
    242    /**
    243     * Un-registers a WikiEventListener with this instance.
    244     *
    245     * @param listener the event listener
    246     * @since 2.4.75
    247     */
    248    public final synchronized void removeWikiEventListener( final WikiEventListener listener ) {
    249        WikiEventManager.removeWikiEventListener( this, listener );
    250    }
    251
    252    /**
    253     * Fires a WikiSecurityEvent to all registered listeners.
    254     *
    255     * @param type  the event type
    256     * @param principal the user principal associated with this session
    257     * @param session the wiki session
    258     * @since 2.4.75
    259     */
    260    protected final void fireEvent( final int type, final Principal principal, final Session session ) {
    261        if( WikiEventManager.isListening( this ) ) {
    262            WikiEventManager.fireEvent( this, new WikiSecurityEvent( this, type, principal, session ) );
    263        }
    264    }
    265
    266    /**
    267     * Fires when the web container creates a new HTTP session.
    268     *
    269     * @param se the HTTP session event
    270     */
    271    @Override
    272    public void sessionCreated( final HttpSessionEvent se ) {
    273        final HttpSession session = se.getSession();
    274        LOG.debug( "Created session: " + session.getId() + "." );
    275    }
    276
    277    /**
    278     * Removes the user's WikiSession from the internal session cache when the web
    279     * container destroys an HTTP session.
    280     * @param se the HTTP session event
    281     */
    282    @Override
    283    public void sessionDestroyed( final HttpSessionEvent se ) {
    284        final HttpSession session = se.getSession();
    285        for( final SessionMonitor monitor : c_monitors.values() ) {
    286            final Session storedSession = monitor.findSession( session );
    287            monitor.remove( session );
    288            LOG.debug( "Removed session " + session.getId() + "." );
    289            if( storedSession != null ) {
    290                fireEvent( WikiSecurityEvent.SESSION_EXPIRED, storedSession.getLoginPrincipal(), storedSession );
    291            }
    292        }
    293    }
    294
    295}