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.shiro.session.mgt;
020
021import org.apache.shiro.session.ExpiredSessionException;
022import org.apache.shiro.session.InvalidSessionException;
023import org.apache.shiro.session.StoppedSessionException;
024import org.apache.shiro.util.CollectionUtils;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028import java.io.IOException;
029import java.io.ObjectInputStream;
030import java.io.ObjectOutputStream;
031import java.io.Serializable;
032import java.text.DateFormat;
033import java.util.Collection;
034import java.util.Collections;
035import java.util.Date;
036import java.util.HashMap;
037import java.util.Map;
038
039
040/**
041 * Simple {@link org.apache.shiro.session.Session} JavaBeans-compatible POJO implementation, intended to be used on the
042 * business/server tier.
043 *
044 * @since 0.1
045 */
046@SuppressWarnings("checkstyle:MethodCount")
047public class SimpleSession implements ValidatingSession, Serializable {
048
049    protected static final long MILLIS_PER_SECOND = 1000;
050    protected static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND;
051    protected static final long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE;
052
053    //serialization bitmask fields. DO NOT CHANGE THE ORDER THEY ARE DECLARED!
054    static int bitIndexCounter;
055
056    // Serialization reminder:
057    // You _MUST_ change this number if you introduce a change to this class
058    // that is NOT serialization backwards compatible.  Serialization-compatible
059    // changes do not require a change to this number.  If you need to generate
060    // a new number in this case, use the JDK's 'serialver' program to generate it.
061    private static final long serialVersionUID = -7125642695178165650L;
062
063    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleSession.class);
064    private static final int ID_BIT_MASK = 1 << bitIndexCounter++;
065    private static final int START_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++;
066    private static final int STOP_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++;
067    private static final int LAST_ACCESS_TIME_BIT_MASK = 1 << bitIndexCounter++;
068    private static final int TIMEOUT_BIT_MASK = 1 << bitIndexCounter++;
069    private static final int EXPIRED_BIT_MASK = 1 << bitIndexCounter++;
070    private static final int HOST_BIT_MASK = 1 << bitIndexCounter++;
071    private static final int ATTRIBUTES_BIT_MASK = 1 << bitIndexCounter++;
072
073    // ==============================================================
074    // NOTICE:
075    //
076    // The following fields are marked as transient to avoid double-serialization.
077    // They are in fact serialized (even though 'transient' usually indicates otherwise),
078    // but they are serialized explicitly via the writeObject and readObject implementations
079    // in this class.
080    //
081    // If we didn't declare them as transient, the out.defaultWriteObject(); call in writeObject would
082    // serialize all non-transient fields as well, effectively doubly serializing the fields (also
083    // doubling the serialization size).
084    //
085    // This finding, with discussion, was covered here:
086    //
087    // http://mail-archives.apache.org/mod_mbox/shiro-user/201109.mbox/%3C4E81BCBD.8060909@metaphysis.net%3E
088    //
089    // ==============================================================
090    private transient Serializable id;
091    private transient Date startTimestamp;
092    private transient Date stopTimestamp;
093    private transient Date lastAccessTime;
094    private transient long timeout;
095    private transient boolean expired;
096    private transient String host;
097    private transient Map<Object, Object> attributes;
098
099    public SimpleSession() {
100        //TODO - remove concrete reference to DefaultSessionManager
101        this.timeout = DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT;
102        this.startTimestamp = new Date();
103        this.lastAccessTime = this.startTimestamp;
104    }
105
106    public SimpleSession(String host) {
107        this();
108        this.host = host;
109    }
110
111    public Serializable getId() {
112        return this.id;
113    }
114
115    public void setId(Serializable id) {
116        this.id = id;
117    }
118
119    public Date getStartTimestamp() {
120        return startTimestamp;
121    }
122
123    public void setStartTimestamp(Date startTimestamp) {
124        this.startTimestamp = startTimestamp;
125    }
126
127    /**
128     * Returns the time the session was stopped, or <tt>null</tt> if the session is still active.
129     * <p/>
130     * A session may become stopped under a number of conditions:
131     * <ul>
132     * <li>If the user logs out of the system, their current session is terminated (released).</li>
133     * <li>If the session expires</li>
134     * <li>The application explicitly calls {@link #stop()}</li>
135     * <li>If there is an internal system error and the session state can no longer accurately
136     * reflect the user's behavior, such in the case of a system crash</li>
137     * </ul>
138     * <p/>
139     * Once stopped, a session may no longer be used.  It is locked from all further activity.
140     *
141     * @return The time the session was stopped, or <tt>null</tt> if the session is still
142     * active.
143     */
144    public Date getStopTimestamp() {
145        return stopTimestamp;
146    }
147
148    public void setStopTimestamp(Date stopTimestamp) {
149        this.stopTimestamp = stopTimestamp;
150    }
151
152    public Date getLastAccessTime() {
153        return lastAccessTime;
154    }
155
156    public void setLastAccessTime(Date lastAccessTime) {
157        this.lastAccessTime = lastAccessTime;
158    }
159
160    /**
161     * Returns true if this session has expired, false otherwise.  If the session has
162     * expired, no further user interaction with the system may be done under this session.
163     *
164     * @return true if this session has expired, false otherwise.
165     */
166    public boolean isExpired() {
167        return expired;
168    }
169
170    public void setExpired(boolean expired) {
171        this.expired = expired;
172    }
173
174    public long getTimeout() {
175        return timeout;
176    }
177
178    public void setTimeout(long timeout) {
179        this.timeout = timeout;
180    }
181
182    public String getHost() {
183        return host;
184    }
185
186    public void setHost(String host) {
187        this.host = host;
188    }
189
190    public Map<Object, Object> getAttributes() {
191        return attributes;
192    }
193
194    public void setAttributes(Map<Object, Object> attributes) {
195        this.attributes = attributes;
196    }
197
198    public void touch() {
199        this.lastAccessTime = new Date();
200    }
201
202    public void stop() {
203        if (this.stopTimestamp == null) {
204            this.stopTimestamp = new Date();
205        }
206    }
207
208    protected boolean isStopped() {
209        return getStopTimestamp() != null;
210    }
211
212    protected void expire() {
213        stop();
214        this.expired = true;
215    }
216
217    /**
218     * @since 0.9
219     */
220    public boolean isValid() {
221        return !isStopped() && !isExpired();
222    }
223
224    /**
225     * Determines if this session is expired.
226     *
227     * @return true if the specified session has expired, false otherwise.
228     */
229    protected boolean isTimedOut() {
230
231        if (isExpired()) {
232            return true;
233        }
234
235        long timeout = getTimeout();
236
237        if (timeout >= 0L) {
238
239            Date lastAccessTime = getLastAccessTime();
240
241            if (lastAccessTime == null) {
242                String msg = "session.lastAccessTime for session with id ["
243                        + getId() + "] is null.  This value must be set at "
244                        + "least once, preferably at least upon instantiation.  Please check the "
245                        + getClass().getName() + " implementation and ensure "
246                        + "this value will be set (perhaps in the constructor?)";
247                throw new IllegalStateException(msg);
248            }
249
250            // Calculate at what time a session would have been last accessed
251            // for it to be expired at this point.  In other words, subtract
252            // from the current time the amount of time that a session can
253            // be inactive before expiring.  If the session was last accessed
254            // before this time, it is expired.
255            long expireTimeMillis = System.currentTimeMillis() - timeout;
256            Date expireTime = new Date(expireTimeMillis);
257            return lastAccessTime.before(expireTime);
258        } else {
259            if (LOGGER.isTraceEnabled()) {
260                LOGGER.trace("No timeout for session with id [" + getId()
261                        + "].  Session is not considered expired.");
262            }
263        }
264
265        return false;
266    }
267
268    public void validate() throws InvalidSessionException {
269        //check for stopped:
270        if (isStopped()) {
271            //timestamp is set, so the session is considered stopped:
272            String msg = "Session with id [" + getId() + "] has been "
273                    + "explicitly stopped.  No further interaction under this session is "
274                    + "allowed.";
275            throw new StoppedSessionException(msg);
276        }
277
278        //check for expiration
279        if (isTimedOut()) {
280            expire();
281
282            //throw an exception explaining details of why it expired:
283            Date lastAccessTime = getLastAccessTime();
284            long timeout = getTimeout();
285
286            Serializable sessionId = getId();
287
288            DateFormat df = DateFormat.getInstance();
289            String msg = "Session with id [" + sessionId + "] has expired. "
290                    + "Last access time: " + df.format(lastAccessTime)
291                    + ".  Current time: " + df.format(new Date())
292                    + ".  Session timeout is set to " + timeout / MILLIS_PER_SECOND + " seconds ("
293                    + timeout / MILLIS_PER_MINUTE + " minutes)";
294            if (LOGGER.isTraceEnabled()) {
295                LOGGER.trace(msg);
296            }
297            throw new ExpiredSessionException(msg);
298        }
299    }
300
301    private Map<Object, Object> getAttributesLazy() {
302        Map<Object, Object> attributes = getAttributes();
303        if (attributes == null) {
304            attributes = new HashMap<Object, Object>();
305            setAttributes(attributes);
306        }
307        return attributes;
308    }
309
310    public Collection<Object> getAttributeKeys() throws InvalidSessionException {
311        Map<Object, Object> attributes = getAttributes();
312        if (attributes == null) {
313            return Collections.emptySet();
314        }
315        return attributes.keySet();
316    }
317
318    public Object getAttribute(Object key) {
319        Map<Object, Object> attributes = getAttributes();
320        if (attributes == null) {
321            return null;
322        }
323        return attributes.get(key);
324    }
325
326    public void setAttribute(Object key, Object value) {
327        if (value == null) {
328            removeAttribute(key);
329        } else {
330            getAttributesLazy().put(key, value);
331        }
332    }
333
334    public Object removeAttribute(Object key) {
335        Map<Object, Object> attributes = getAttributes();
336        if (attributes == null) {
337            return null;
338        } else {
339            return attributes.remove(key);
340        }
341    }
342
343    /**
344     * Returns {@code true} if the specified argument is an {@code instanceof} {@code SimpleSession} and both
345     * {@link #getId() id}s are equal.  If the argument is a {@code SimpleSession} and either 'this' or the argument
346     * does not yet have an ID assigned, the value of {@link #onEquals(SimpleSession) onEquals} is returned, which
347     * does a necessary attribute-based comparison when IDs are not available.
348     * <p/>
349     * Do your best to ensure {@code SimpleSession} instances receive an ID very early in their lifecycle to
350     * avoid the more expensive attributes-based comparison.
351     *
352     * @param obj the object to compare with this one for equality.
353     * @return {@code true} if this object is equivalent to the specified argument, {@code false} otherwise.
354     */
355    @Override
356    public boolean equals(Object obj) {
357        if (this == obj) {
358            return true;
359        }
360        if (obj instanceof SimpleSession) {
361            SimpleSession other = (SimpleSession) obj;
362            Serializable thisId = getId();
363            Serializable otherId = other.getId();
364            if (thisId != null && otherId != null) {
365                return thisId.equals(otherId);
366            } else {
367                //fall back to an attribute based comparison:
368                return onEquals(other);
369            }
370        }
371        return false;
372    }
373
374    /**
375     * Provides an attribute-based comparison (no ID comparison) - incurred <em>only</em> when 'this' or the
376     * session object being compared for equality do not have a session id.
377     *
378     * @param ss the SimpleSession instance to compare for equality.
379     * @return true if all the attributes, except the id, are equal to this object's attributes.
380     * @since 1.0
381     */
382    @SuppressWarnings({"checkstyle:BooleanExpressionComplexity", "checkstyle:MethodCount"})
383    protected boolean onEquals(SimpleSession ss) {
384        return (getStartTimestamp() != null ? getStartTimestamp().equals(ss.getStartTimestamp()) : ss.getStartTimestamp() == null)
385                && (getStopTimestamp() != null ? getStopTimestamp().equals(ss.getStopTimestamp()) : ss.getStopTimestamp() == null)
386                && (getLastAccessTime() != null
387                        ? getLastAccessTime().equals(ss.getLastAccessTime()) : ss.getLastAccessTime() == null)
388                && (getTimeout() == ss.getTimeout())
389                && (isExpired() == ss.isExpired())
390                && (getHost() != null ? getHost().equals(ss.getHost()) : ss.getHost() == null)
391                && (getAttributes() != null ? getAttributes().equals(ss.getAttributes()) : ss.getAttributes() == null);
392    }
393
394    /**
395     * Returns the hashCode.  If the {@link #getId() id} is not {@code null}, its hashcode is returned immediately.
396     * If it is {@code null}, an attributes-based hashCode will be calculated and returned.
397     * <p/>
398     * Do your best to ensure {@code SimpleSession} instances receive an ID very early in their lifecycle to
399     * avoid the more expensive attributes-based calculation.
400     *
401     * @return this object's hashCode
402     * @since 1.0
403     */
404    @Override
405    public int hashCode() {
406        Serializable id = getId();
407        if (id != null) {
408            return id.hashCode();
409        }
410        int hashCode = getStartTimestamp() != null ? getStartTimestamp().hashCode() : 0;
411        hashCode = 31 * hashCode + (getStopTimestamp() != null ? getStopTimestamp().hashCode() : 0);
412        hashCode = 31 * hashCode + (getLastAccessTime() != null ? getLastAccessTime().hashCode() : 0);
413        hashCode = 31 * hashCode + Long.valueOf(Math.max(getTimeout(), 0)).hashCode();
414        hashCode = 31 * hashCode + Boolean.valueOf(isExpired()).hashCode();
415        hashCode = 31 * hashCode + (getHost() != null ? getHost().hashCode() : 0);
416        hashCode = 31 * hashCode + (getAttributes() != null ? getAttributes().hashCode() : 0);
417        return hashCode;
418    }
419
420    /**
421     * Returns the string representation of this SimpleSession, equal to
422     * <code>getClass().getName() + &quot;,id=&quot; + getId()</code>.
423     *
424     * @return the string representation of this SimpleSession, equal to
425     * <code>getClass().getName() + &quot;,id=&quot; + getId()</code>.
426     * @since 1.0
427     */
428    @Override
429    public String toString() {
430        StringBuilder sb = new StringBuilder();
431        sb.append(getClass().getName()).append(",id=").append(getId());
432        return sb.toString();
433    }
434
435    /**
436     * Serializes this object to the specified output stream for JDK Serialization.
437     *
438     * @param out output stream used for Object serialization.
439     * @throws IOException if any of this object's fields cannot be written to the stream.
440     * @since 1.0
441     */
442    @SuppressWarnings("checkstyle:NPathComplexity")
443    private void writeObject(ObjectOutputStream out) throws IOException {
444        out.defaultWriteObject();
445        short alteredFieldsBitMask = getAlteredFieldsBitMask();
446        out.writeShort(alteredFieldsBitMask);
447        if (id != null) {
448            out.writeObject(id);
449        }
450        if (startTimestamp != null) {
451            out.writeObject(startTimestamp);
452        }
453        if (stopTimestamp != null) {
454            out.writeObject(stopTimestamp);
455        }
456        if (lastAccessTime != null) {
457            out.writeObject(lastAccessTime);
458        }
459        if (timeout != 0L) {
460            out.writeLong(timeout);
461        }
462        if (expired) {
463            out.writeBoolean(expired);
464        }
465        if (host != null) {
466            out.writeUTF(host);
467        }
468        if (!CollectionUtils.isEmpty(attributes)) {
469            out.writeObject(attributes);
470        }
471    }
472
473    /**
474     * Reconstitutes this object based on the specified InputStream for JDK Serialization.
475     *
476     * @param in the input stream to use for reading data to populate this object.
477     * @throws IOException            if the input stream cannot be used.
478     * @throws ClassNotFoundException if a required class needed for instantiation is not available in the present JVM
479     * @since 1.0
480     */
481    @SuppressWarnings({"unchecked", "checkstyle:NPathComplexity"})
482    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
483        in.defaultReadObject();
484        short bitMask = in.readShort();
485
486        if (isFieldPresent(bitMask, ID_BIT_MASK)) {
487            this.id = (Serializable) in.readObject();
488        }
489        if (isFieldPresent(bitMask, START_TIMESTAMP_BIT_MASK)) {
490            this.startTimestamp = (Date) in.readObject();
491        }
492        if (isFieldPresent(bitMask, STOP_TIMESTAMP_BIT_MASK)) {
493            this.stopTimestamp = (Date) in.readObject();
494        }
495        if (isFieldPresent(bitMask, LAST_ACCESS_TIME_BIT_MASK)) {
496            this.lastAccessTime = (Date) in.readObject();
497        }
498        if (isFieldPresent(bitMask, TIMEOUT_BIT_MASK)) {
499            this.timeout = in.readLong();
500        }
501        if (isFieldPresent(bitMask, EXPIRED_BIT_MASK)) {
502            this.expired = in.readBoolean();
503        }
504        if (isFieldPresent(bitMask, HOST_BIT_MASK)) {
505            this.host = in.readUTF();
506        }
507        if (isFieldPresent(bitMask, ATTRIBUTES_BIT_MASK)) {
508            this.attributes = (Map<Object, Object>) in.readObject();
509        }
510    }
511
512    /**
513     * Returns a bit mask used during serialization indicating which fields have been serialized. Fields that have been
514     * altered (not null and/or not retaining the class defaults) will be serialized and have 1 in their respective
515     * index, fields that are null and/or retain class default values have 0.
516     *
517     * @return a bit mask used during serialization indicating which fields have been serialized.
518     * @since 1.0
519     */
520    @SuppressWarnings("checkstyle:NPathComplexity")
521    private short getAlteredFieldsBitMask() {
522        int bitMask = 0;
523        bitMask = id != null ? bitMask | ID_BIT_MASK : bitMask;
524        bitMask = startTimestamp != null ? bitMask | START_TIMESTAMP_BIT_MASK : bitMask;
525        bitMask = stopTimestamp != null ? bitMask | STOP_TIMESTAMP_BIT_MASK : bitMask;
526        bitMask = lastAccessTime != null ? bitMask | LAST_ACCESS_TIME_BIT_MASK : bitMask;
527        bitMask = timeout != 0L ? bitMask | TIMEOUT_BIT_MASK : bitMask;
528        bitMask = expired ? bitMask | EXPIRED_BIT_MASK : bitMask;
529        bitMask = host != null ? bitMask | HOST_BIT_MASK : bitMask;
530        bitMask = !CollectionUtils.isEmpty(attributes) ? bitMask | ATTRIBUTES_BIT_MASK : bitMask;
531        return (short) bitMask;
532    }
533
534    /**
535     * Returns {@code true} if the given {@code bitMask} argument indicates that the specified field has been
536     * serialized and therefore should be read during deserialization, {@code false} otherwise.
537     *
538     * @param bitMask      the aggregate bitmask for all fields that have been serialized.  Individual bits represent
539     *                     the fields that have been serialized.  A bit set to 1 means that corresponding field has
540     *                     been serialized, 0 means it hasn't been serialized.
541     * @param fieldBitMask the field bit mask constant identifying which bit to inspect (corresponds to a class attribute).
542     * @return {@code true} if the given {@code bitMask} argument indicates that the specified field has been
543     * serialized and therefore should be read during deserialization, {@code false} otherwise.
544     * @since 1.0
545     */
546    private static boolean isFieldPresent(short bitMask, int fieldBitMask) {
547        return (bitMask & fieldBitMask) != 0;
548    }
549
550}