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.ArrayList;
037import java.util.Arrays;
038import java.util.Collection;
039import java.util.Map;
040import java.util.WeakHashMap;
041import java.util.concurrent.ConcurrentHashMap;
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            if( log.isDebugEnabled() ) {
118                log.debug( "Looking up WikiSession for session ID=" + sid + "... found it" );
119            }
120            wikiSession = storedSession;
121        }
122
123        return wikiSession;
124    }
125
126    /**
127     * <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
128     * "guest session" as constructed by {@link org.apache.wiki.api.spi.SessionSPI#guest(Engine)} if the HttpSession is not currently
129     * associated with a WikiSession. This method is guaranteed to return a non-<code>null</code> WikiSession.</p>
130     * <p>Internally, the session is stored in a HashMap; keys are the HttpSession objects, while the values are
131     * {@link java.lang.ref.WeakReference}-wrapped WikiSessions.</p>
132     *
133     * @param session the HTTP session
134     * @return the wiki session
135     */
136    public final Session find( final HttpSession session ) {
137        final Session wikiSession = findSession( session );
138        final String sid = ( session == null ) ? "(null)" : session.getId();
139        if( wikiSession == null ) {
140            return createGuestSessionFor( sid );
141        }
142
143        return wikiSession;
144    }
145
146    /**
147     * <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
148     * "guest session" as constructed by {@link org.apache.wiki.api.spi.SessionSPI#guest(Engine)} if the HttpSession is not currently
149     * associated with a WikiSession. This method is guaranteed to return a non-<code>null</code> WikiSession.</p>
150     * <p>Internally, the session is stored in a HashMap; keys are the HttpSession objects, while the values are
151     * {@link java.lang.ref.WeakReference}-wrapped WikiSessions.</p>
152     *
153     * @param sessionId the HTTP session
154     * @return the wiki session
155     */
156    public final Session find( final String sessionId ) {
157        final Session wikiSession = findSession( sessionId );
158        if( wikiSession == null ) {
159            return createGuestSessionFor( sessionId );
160        }
161
162        return wikiSession;
163    }
164
165    /**
166     * Creates a new session and stashes it
167     *
168     * @param sessionId id looked for before creating the guest session
169     * @return a new guest session
170     */
171    private Session createGuestSessionFor( final String sessionId ) {
172        if( log.isDebugEnabled() ) {
173            log.debug( "Session for session ID=" + sessionId + "... not found. Creating guestSession()" );
174        }
175        final Session wikiSession = Wiki.session().guest( m_engine );
176        synchronized( m_sessions ) {
177            m_sessions.put( sessionId, wikiSession );
178        }
179        return wikiSession;
180    }
181
182    /**
183     * Removes the wiki session associated with the user's HttpRequest from the session cache.
184     *
185     * @param request the user's HTTP request
186     */
187    public final void remove( final HttpServletRequest request ) {
188        if( request == null ) {
189            throw new IllegalArgumentException( "Request cannot be null." );
190        }
191        remove( request.getSession() );
192    }
193
194    /**
195     * Removes the wiki session associated with the user's HttpSession from the session cache.
196     *
197     * @param session the user's HTTP session
198     */
199    public final void remove( final HttpSession session ) {
200        if( session == null ) {
201            throw new IllegalArgumentException( "Session cannot be null." );
202        }
203        synchronized( m_sessions ) {
204            m_sessions.remove( session.getId() );
205        }
206    }
207
208    /**
209     * Returns the current number of active wiki sessions.
210     * @return the number of sessions
211     */
212    public final int sessions()
213    {
214        return userPrincipals().length;
215    }
216
217    /**
218     * <p>Returns the current wiki users as a sorted array of Principal objects. The principals are those returned by
219     * each WikiSession's {@link Session#getUserPrincipal()}'s method.</p>
220     * <p>To obtain the list of current WikiSessions, we iterate through our session Map and obtain the list of values,
221     * which are WikiSessions wrapped in {@link java.lang.ref.WeakReference} objects. Those <code>WeakReference</code>s
222     * whose <code>get()</code> method returns non-<code>null</code> values are valid sessions.</p>
223     *
224     * @return the array of user principals
225     */
226    public final Principal[] userPrincipals() {
227        final Collection<Principal> principals = new ArrayList<>();
228        synchronized ( m_sessions ) {
229            for ( final Session session : m_sessions.values()) {
230                principals.add( session.getUserPrincipal() );
231            }
232        }
233        final Principal[] p = principals.toArray( new Principal[0] );
234        Arrays.sort( p, m_comparator );
235        return p;
236    }
237
238    /**
239     * Registers a WikiEventListener with this instance.
240     *
241     * @param listener the event listener
242     * @since 2.4.75
243     */
244    public final synchronized void addWikiEventListener( final WikiEventListener listener ) {
245        WikiEventManager.addWikiEventListener( this, listener );
246    }
247
248    /**
249     * Un-registers a WikiEventListener with this instance.
250     *
251     * @param listener the event listener
252     * @since 2.4.75
253     */
254    public final synchronized void removeWikiEventListener( final WikiEventListener listener ) {
255        WikiEventManager.removeWikiEventListener( this, listener );
256    }
257
258    /**
259     * Fires a WikiSecurityEvent to all registered listeners.
260     *
261     * @param type  the event type
262     * @param principal the user principal associated with this session
263     * @param session the wiki session
264     * @since 2.4.75
265     */
266    protected final void fireEvent( final int type, final Principal principal, final Session session ) {
267        if( WikiEventManager.isListening( this ) ) {
268            WikiEventManager.fireEvent( this, new WikiSecurityEvent( this, type, principal, session ) );
269        }
270    }
271
272    /**
273     * Fires when the web container creates a new HTTP session.
274     * 
275     * @param se the HTTP session event
276     */
277    @Override
278    public void sessionCreated( final HttpSessionEvent se ) {
279        final HttpSession session = se.getSession();
280        log.debug( "Created session: " + session.getId() + "." );
281    }
282
283    /**
284     * Removes the user's WikiSession from the internal session cache when the web
285     * container destroys an HTTP session.
286     * @param se the HTTP session event
287     */
288    @Override
289    public void sessionDestroyed( final HttpSessionEvent se ) {
290        final HttpSession session = se.getSession();
291        for( final SessionMonitor monitor : c_monitors.values() ) {
292            final Session storedSession = monitor.findSession( session );
293            monitor.remove( session );
294            log.debug( "Removed session " + session.getId() + "." );
295            if( storedSession != null ) {
296                fireEvent( WikiSecurityEvent.SESSION_EXPIRED, storedSession.getLoginPrincipal(), storedSession );
297            }
298        }
299    }
300
301}