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}