1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements.  See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership.  The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License.  You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing,
13  * software distributed under the License is distributed on an
14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15  * KIND, either express or implied.  See the License for the
16  * specific language governing permissions and limitations
17  * under the License.
18  */
19 module hunt.shiro.session.mgt.SimpleSession;
20 
21 import hunt.shiro.session.mgt.ValidatingSession;
22 import hunt.shiro.session.mgt.DefaultSessionManager;
23 import hunt.shiro.session.Session;
24 
25 import hunt.shiro.Exceptions;
26 import hunt.shiro.util.CollectionUtils;
27 
28 import hunt.collection;
29 import hunt.Exceptions;
30 import hunt.logging.Logger;
31 import hunt.util.StringBuilder;
32 import hunt.util.Common;
33 import hunt.util.DateTime;
34 
35 import hunt.Long;
36 import std.array;
37 
38 
39 /**
40  * Simple {@link hunt.shiro.session.Session} JavaBeans-compatible POJO implementation, intended to be used on the
41  * business/server tier.
42  *
43  */
44 class SimpleSession : ValidatingSession {
45 
46     // Serialization reminder:
47     // You _MUST_ change this number if you introduce a change to this class
48     // that is NOT serialization backwards compatible.  Serialization-compatible
49     // changes do not require a change to this number.  If you need to generate
50     // a new number in this case, use the JDK's 'serialver' program to generate it.
51 
52     protected enum long MILLIS_PER_SECOND = 1000;
53     protected enum long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND;
54     protected enum long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE;
55 
56     //serialization bitmask fields. DO NOT CHANGE THE ORDER THEY ARE DECLARED!
57     enum int bitIndexCounter = 0;
58     private enum int ID_BIT_MASK = 1 << 0;
59     private enum int START_TIMESTAMP_BIT_MASK = 1 << 1;
60     private enum int STOP_TIMESTAMP_BIT_MASK = 1 << 2;
61     private enum int LAST_ACCESS_TIME_BIT_MASK = 1 << 3;
62     private enum int TIMEOUT_BIT_MASK = 1 << 4;
63     private enum int EXPIRED_BIT_MASK = 1 << 5;
64     private enum int HOST_BIT_MASK = 1 << 6;
65     private enum int ATTRIBUTES_BIT_MASK = 1 << 7;
66 
67     // ==============================================================
68     // NOTICE:
69     //
70     // The following fields are marked as  to avoid double-serialization.
71     // They are in fact serialized (even though '' usually indicates otherwise),
72     // but they are serialized explicitly via the writeObject and readObject implementations
73     // in this class.
74     //
75     // If we didn't declare them as , the out.defaultWriteObject(); call in writeObject would
76     // serialize all non- fields as well, effectively doubly serializing the fields (also
77     // doubling the serialization size).
78     //
79     // This finding, with discussion, was covered here:
80     //
81     // http://mail-archives.apache.org/mod_mbox/shiro-user/201109.mbox/%3C4E81BCBD.8060909@metaphysis.net%3E
82     //
83     // ==============================================================
84     private  string id;
85     private  Date startTimestamp;
86     private  Long stopTimestamp;
87     private  Date lastAccessTime;
88     private  long timeout;
89     private  bool expired;
90     private  string host;
91     private  Map!(Object, Object) attributes;
92 
93     this() {
94         this.timeout = DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT; //TODO - remove concrete reference to DefaultSessionManager
95         this.startTimestamp = DateTime.currentTimeMillis();
96         this.lastAccessTime = this.startTimestamp;
97     }
98 
99     this(string host) {
100         this();
101         this.host = host;
102     }
103 
104     string getId() @trusted nothrow {
105         return this.id;
106     }
107 
108      void setId(string id) @trusted nothrow {
109         this.id = id;
110     }
111 
112      Date getStartTimestamp() {
113         return startTimestamp;
114     }
115 
116      void setStartTimestamp(Date startTimestamp) {
117         this.startTimestamp = startTimestamp;
118     }
119 
120     /**
121      * Returns the time the session was stopped, or <tt>null</tt> if the session is still active.
122      * <p/>
123      * A session may become stopped under a number of conditions:
124      * <ul>
125      * <li>If the user logs out of the system, their current session is terminated (released).</li>
126      * <li>If the session expires</li>
127      * <li>The application explicitly calls {@link #stop()}</li>
128      * <li>If there is an internal system error and the session state can no longer accurately
129      * reflect the user's behavior, such in the case of a system crash</li>
130      * </ul>
131      * <p/>
132      * Once stopped, a session may no longer be used.  It is locked from all further activity.
133      *
134      * @return The time the session was stopped, or <tt>null</tt> if the session is still
135      *         active.
136      */
137     Long getStopTimestamp() {
138         return stopTimestamp;
139     }
140 
141      void setStopTimestamp(Long stopTimestamp) {
142         this.stopTimestamp = stopTimestamp;
143     }
144 
145     Date getLastAccessTime() {
146         return lastAccessTime;
147     }
148 
149      void setLastAccessTime(Date lastAccessTime) {
150         this.lastAccessTime = lastAccessTime;
151     }
152 
153     /**
154      * Returns true if this session has expired, false otherwise.  If the session has
155      * expired, no further user interaction with the system may be done under this session.
156      *
157      * @return true if this session has expired, false otherwise.
158      */
159      bool isExpired() {
160         return expired;
161     }
162 
163      void setExpired(bool expired) {
164         this.expired = expired;
165     }
166 
167      long getTimeout() {
168         return timeout;
169     }
170 
171      void setTimeout(long timeout) {
172         this.timeout = timeout;
173     }
174 
175      string getHost() {
176         return host;
177     }
178 
179      void setHost(string host) {
180         this.host = host;
181     }
182 
183     Map!(Object, Object) getAttributes() {
184         return attributes;
185     }
186 
187     void setAttributes(Map!(Object, Object) attributes) {
188         this.attributes = attributes;
189     }
190 
191     void touch() {
192         this.lastAccessTime = DateTime.currentTimeMillis();
193     }
194 
195     void stop() {
196         if (this.stopTimestamp is null) {
197             this.stopTimestamp = new Long(DateTime.currentTimeMillis());
198         }
199     }
200 
201     protected bool isStopped() {
202         return getStopTimestamp() !is null;
203     }
204 
205     protected void expire() {
206         stop();
207         this.expired = true;
208     }
209 
210     /**
211      */
212      bool isValid() {
213         return !isStopped() && !isExpired();
214     }
215 
216     /**
217      * Determines if this session is expired.
218      *
219      * @return true if the specified session has expired, false otherwise.
220      */
221     protected bool isTimedOut() {
222 
223         if (isExpired()) {
224             return true;
225         }
226 
227         long timeout = getTimeout();
228 
229         if (timeout >= 0) {
230 
231             Date lastAccessTime = getLastAccessTime();
232 
233             // if (lastAccessTime is null) {
234             //     string msg = "session.lastAccessTime for session with id [" ~
235             //             getId() ~ "] is null.  This value must be set at " ~
236             //             "least once, preferably at least upon instantiation.  Please check the " ~
237             //             typeid(this).name ~ " implementation and ensure " ~
238             //             "this value will be set (perhaps in the constructor?)";
239             //     throw new IllegalStateException(msg);
240             // }
241 
242             // Calculate at what time a session would have been last accessed
243             // for it to be expired at this point.  In other words, subtract
244             // from the current time the amount of time that a session can
245             // be inactive before expiring.  If the session was last accessed
246             // before this time, it is expired.
247             long expireTimeMillis = DateTime.currentTimeMillis() - timeout;
248             return lastAccessTime < expireTimeMillis;
249         } else {
250             version(HUNT_SHIRO_DEBUG) {
251                 tracef("No timeout for session with id [" ~ getId() ~
252                         "].  Session is not considered expired.");
253             }
254         }
255 
256         return false;
257     }
258 
259      void validate(){
260         //check for stopped:
261         if (isStopped()) {
262             //timestamp is set, so the session is considered stopped:
263             string msg = "Session with id [" ~ getId() ~ "] has been " ~
264                     "explicitly stopped.  No further interaction under this session is " ~
265                     "allowed.";
266             throw new StoppedSessionException(msg);
267         }
268 
269         //check for expiration
270         if (isTimedOut()) {
271             expire();
272 
273             //throw an exception explaining details of why it expired:
274             Date lastAccessTime = getLastAccessTime();
275             long timeout = getTimeout();
276             string sessionId = getId();
277 
278             // DateFormat df = DateFormat.getInstance();
279             import std.conv;
280             string msg = "Session with id [" ~ sessionId ~ "] has expired. " ~
281                     "Last access time: " ~ lastAccessTime.to!string() ~
282                     ".  Current time: " ~ DateTime.currentTimeMillis().to!string() ~
283                     ".  Session timeout is set to " ~ to!string(timeout / MILLIS_PER_SECOND) ~ " seconds (" ~
284                     to!string(timeout / MILLIS_PER_MINUTE) ~ " minutes)";
285             version(HUNT_DEBUG) {
286                 warningf(msg);
287             }
288             throw new ExpiredSessionException(msg);
289         }
290     }
291 
292     private Map!(Object, Object) getAttributesLazy() {
293         Map!(Object, Object) attributes = getAttributes();
294         if (attributes  is null) {
295             attributes = new HashMap!(Object, Object)();
296             setAttributes(attributes);
297         }
298         return attributes;
299     }
300 
301     Object[] getAttributeKeys(){
302         Map!(Object, Object) attributes = getAttributes();
303         if (attributes is null) {
304             return null;
305         }
306         return attributes.byKey.array; // .keySet();
307     }
308 
309     Object getAttribute(Object key) {
310         Map!(Object, Object) attributes = getAttributes();
311         if (attributes  is null) {
312             return null;
313         }
314         return attributes.get(key);
315     }
316 
317     void setAttribute(Object key, Object value) {
318         if (value  is null) {
319             removeAttribute(key);
320         } else {
321             getAttributesLazy().put(key, value);
322         }
323     }
324 
325     Object removeAttribute(Object key) {
326         Map!(Object, Object) attributes = getAttributes();
327         if (attributes  is null) {
328             return null;
329         } else {
330             return attributes.remove(key);
331         }
332     }
333 
334     /**
335      * Returns {@code true} if the specified argument is an {@code instanceof} {@code SimpleSession} and both
336      * {@link #getId() id}s are equal.  If the argument is a {@code SimpleSession} and either 'this' or the argument
337      * does not yet have an ID assigned, the value of {@link #onEquals(SimpleSession) onEquals} is returned, which
338      * does a necessary attribute-based comparison when IDs are not available.
339      * <p/>
340      * Do your best to ensure {@code SimpleSession} instances receive an ID very early in their lifecycle to
341      * avoid the more expensive attributes-based comparison.
342      *
343      * @param obj the object to compare with this one for equality.
344      * @return {@code true} if this object is equivalent to the specified argument, {@code false} otherwise.
345      */
346     override
347     bool opEquals(Object obj) {
348         if (this == obj) {
349             return true;
350         }
351 
352         SimpleSession other = cast(SimpleSession) obj;
353         if (other !is null) {
354             string thisId = getId();
355             string otherId = other.getId();
356             if (thisId !is null && otherId !is null) {
357                 return thisId == otherId;
358             } else {
359                 //fall back to an attribute based comparison:
360                 return onEquals(other);
361             }
362         }
363         return false;
364     }
365 
366     /**
367      * Provides an attribute-based comparison (no ID comparison) - incurred <em>only</em> when 'this' or the
368      * session object being compared for equality do not have a session id.
369      *
370      * @param ss the SimpleSession instance to compare for equality.
371      * @return true if all the attributes, except the id, are equal to this object's attributes.
372      */
373     protected bool onEquals(SimpleSession ss) {
374         implementationMissing(false);
375         return true;
376         // return (getStartTimestamp() !is null ? getStartTimestamp()== ss.getStartTimestamp() : ss.getStartTimestamp()  is null) &&
377         //         (getStopTimestamp() !is null ? getStopTimestamp()== ss.getStopTimestamp() : ss.getStopTimestamp()  is null) &&
378         //         (getLastAccessTime() !is null ? getLastAccessTime()== ss.getLastAccessTime() : ss.getLastAccessTime()  is null) &&
379         //         (getTimeout() == ss.getTimeout()) &&
380         //         (isExpired() == ss.isExpired()) &&
381         //         (getHost() !is null ? getHost()== ss.getHost() : ss.getHost()  is null) &&
382         //         (getAttributes() !is null ? getAttributes()== ss.getAttributes() : ss.getAttributes()  is null);
383     }
384 
385     /**
386      * Returns the hashCode.  If the {@link #getId() id} is not {@code null}, its hashcode is returned immediately.
387      * If it is {@code null}, an attributes-based hashCode will be calculated and returned.
388      * <p/>
389      * Do your best to ensure {@code SimpleSession} instances receive an ID very early in their lifecycle to
390      * avoid the more expensive attributes-based calculation.
391      *
392      * @return this object's hashCode
393      */
394     override
395     size_t toHash() @trusted nothrow {
396         string id = getId();
397         if (id !is null) {
398             return hashOf(id);
399         }
400         size_t hashCode;
401         try {
402             implementationMissing(false);
403         } catch(Exception) {
404             
405         }
406         // size_t hashCode = getStartTimestamp() is null ? getStartTimestamp().hashCode() : 0;
407         // hashCode = 31 * hashCode + (getStopTimestamp() !is null ? getStopTimestamp().hashCode() : 0);
408         // hashCode = 31 * hashCode + (getLastAccessTime() !is null ? getLastAccessTime().hashCode() : 0);
409         // hashCode = 31 * hashCode + Long.valueOf(max(getTimeout(), 0)).hashCode();
410         // hashCode = 31 * hashCode + bool.valueOf(isExpired()).hashCode();
411         // hashCode = 31 * hashCode + (getHost() !is null ? getHost().hashCode() : 0);
412         // hashCode = 31 * hashCode + (getAttributes() !is null ? getAttributes().hashCode() : 0);
413         return hashCode;
414     }
415 
416     /**
417      * Returns the string representation of this SimpleSession, equal to
418      * <typeid(code).name + &quot;,id=&quot; + getId()</code>.
419      *
420      * @return the string representation of this SimpleSession, equal to
421      *         <typeid(code).name + &quot;,id=&quot; + getId()</code>.
422      */
423     override
424     string toString() {
425         StringBuilder sb = new StringBuilder();
426         sb.append(typeid(this).name).append(", id=").append(id);
427         return sb.toString();
428     }
429 
430     /**
431      * Returns {@code true} if the given {@code bitMask} argument indicates that the specified field has been
432      * serialized and therefore should be read during deserialization, {@code false} otherwise.
433      *
434      * @param bitMask      the aggregate bitmask for all fields that have been serialized.  Individual bits represent
435      *                     the fields that have been serialized.  A bit set to 1 means that corresponding field has
436      *                     been serialized, 0 means it hasn't been serialized.
437      * @param fieldBitMask the field bit mask constant identifying which bit to inspect (corresponds to a class attribute).
438      * @return {@code true} if the given {@code bitMask} argument indicates that the specified field has been
439      *         serialized and therefore should be read during deserialization, {@code false} otherwise.
440      */
441     private static bool isFieldPresent(short bitMask, int fieldBitMask) {
442         return (bitMask & fieldBitMask) != 0;
443     }
444 
445 }