/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jooby; import static java.util.Objects.requireNonNull; import java.security.SecureRandom; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import com.google.common.io.BaseEncoding; /** * <p> * Sessions are created on demand via: {@link Request#session()}. * </p> * * <p> * Sessions have a lot of uses cases but most commons are: auth, store information about current * user, etc. * </p> * * <p> * A session attribute must be {@link String} or a primitive. Session doesn't allow to store * arbitrary objects. It is a simple mechanism to store basic data. * </p> * * <h1>Session configuration</h1> * * <h2>No timeout</h2> * <p> * There is no timeout for sessions from server perspective. By default, a session will expire when * the user close the browser (a.k.a session cookie). * </p> * * <h2>Session store</h2> * <p> * A {@link Session.Store} is responsible for saving session data. Sessions are kept in memory, by * default using the {@link Session.Mem} store, which is useful for development, but wont scale well * on production environments. An redis, memcached, ehcache store will be a better option. * </p> * * <h3>Store life-cycle</h3> * <p> * Sessions are persisted every time a request exit, if they are dirty. A session get dirty if an * attribute is added or removed from it. * </p> * <p> * The <code>session.saveInterval</code> property indicates how frequently a session will be * persisted (in millis). * </p> * <p> * In short, a session is persisted when: 1) it is dirty; or 2) save interval has expired it. * </p> * * <h1>Cookie configuration</h1> * <p> * Next session describe the most important options: * </p> * * <h2>max-age</h2> * <p> * The <code>session.cookie.maxAge</code> sets the maximum age in seconds. A positive value * indicates that the cookie will expire after that many seconds have passed. Note that the value is * the <i>maximum</i> age when the cookie will expire, not the cookie's current age. * * A negative value means that the cookie is not stored persistently and will be deleted when the * Web browser exits. * * Default maxAge is: <code>-1</code>. * * </p> * * <h2>signed cookie</h2> * <p> * If the <code>application.secret</code> property has been set, then the session cookie will be * signed it with it. * </p> * * <h2>cookie's name</h2> * <p> * The <code>session.cookie.name</code> indicates the name of the cookie that hold the session ID, * by defaults: <code>jooby.sid</code>. Cookie's name can be explicitly set with * {@link Cookie.Definition#name(String)} on {@link Session.Definition#cookie()}. * </p> * * @author edgar * @since 0.1.0 */ public interface Session { /** Global/Shared id of cookie sessions. */ String COOKIE_SESSION = "cookieSession"; /** * Hold session related configuration parameters. * * @author edgar * @since 0.1.0 */ class Definition { /** Session store. */ private Object store; /** Session cookie. */ private Cookie.Definition cookie; /** Save interval. */ private Long saveInterval; /** * Creates a new session definition. * * @param store A session store. */ public Definition(final Class<? extends Store> store) { this.store = requireNonNull(store, "A session store is required."); cookie = new Cookie.Definition(); } /** * Creates a new session definition with a client store. */ Definition() { cookie = new Cookie.Definition(); } /** * Creates a new session definition. * * @param store A session store. */ public Definition(final Store store) { this.store = requireNonNull(store, "A session store is required."); cookie = new Cookie.Definition(); } /** * Indicates how frequently a no-dirty session should be persisted (in millis). * * @return A save interval that indicates how frequently no dirty session should be persisted. */ public Optional<Long> saveInterval() { return Optional.ofNullable(saveInterval); } /** * Set/override how frequently a no-dirty session should be persisted (in millis). * * @param saveInterval Save interval in millis or <code>-1</code> for turning it off. * @return This definition. */ public Definition saveInterval(final long saveInterval) { this.saveInterval = saveInterval; return this; } /** * @return A session store instance or class. */ public Object store() { return store; } /** * @return Configure cookie session. */ public Cookie.Definition cookie() { return cookie; } } /** * Read, save and delete sessions from a persistent storage. * * @author edgar * @since 0.1.0 */ interface Store { /** Single secure random instance. */ SecureRandom rnd = new SecureRandom(); /** * Get a session by ID (if any). * * @param builder A session builder. * @return A session or <code>null</code>. */ Session get(Session.Builder builder); /** * Save/persist a session. * * @param session A session to be persisted. */ void save(Session session); void create(final Session session); /** * Delete a session by ID. * * @param id A session ID. */ void delete(String id); /** * Generate a session ID. * * @return A unique session ID. */ default String generateID() { byte[] bytes = new byte[30]; rnd.nextBytes(bytes); return BaseEncoding.base64Url().encode(bytes); } } /** * A keep in memory session store. * * @author edgar */ class Mem implements Store { private ConcurrentMap<String, Session> sessions = new ConcurrentHashMap<String, Session>(); @Override public void create(final Session session) { sessions.putIfAbsent(session.id(), session); } @Override public void save(final Session session) { sessions.put(session.id(), session); } @Override public Session get(final Session.Builder builder) { return sessions.get(builder.sessionId()); } @Override public void delete(final String id) { sessions.remove(id); } } /** * Build or restore a session from a persistent storage. * * @author edgar */ interface Builder { /** * @return Session ID. */ String sessionId(); /** * Set a session local attribute. * * @param name Attribute's name. * @param value Attribute's value. * @return This builder. */ Builder set(final String name, final String value); /** * Set one ore more session local attributes. * * @param attributes Attributes to add. * @return This builder. */ Builder set(final Map<String, String> attributes); /** * Set session created date. * * @param createdAt Session created date. * @return This builder. */ Builder createdAt(long createdAt); /** * Set session last accessed date. * * @param accessedAt Session last accessed date. * @return This builder. */ Builder accessedAt(long accessedAt); /** * Set session last saved it date. * * @param savedAt Session last saved it date. * @return This builder. */ Builder savedAt(final long savedAt); /** * Final step to build a new session. * * @return A session. */ Session build(); } /** * A session ID for server side sessions. Otherwise {@link #COOKIE_SESSION} for client side sessions. * * Session ID on client sessions doesn't make sense because resolution of session is done via * cookie name. * * Another reason of not saving the session ID inside the cookie, is the cookie size (up to 4kb). * If the session ID is persisted then users lost space to save business data. * * @return Session ID. */ String id(); /** * The time when this session was created, measured in milliseconds since midnight January 1, 1970 * GMT for server side sessions. Or <code>-1</code> for client side sessions. * * @return The time when this session was created, measured in milliseconds since midnight January * 1, 1970 GMT for server side sessions. Or <code>-1</code> for client side sessions. */ long createdAt(); /** * Last time the session was save it as epoch millis or <code>-1</code> for client side sessions. * * @return Last time the session was save it as epoch millis or <code>-1</code> for client side * sessions. */ long savedAt(); /** * The last time the client sent a request associated with this session, as the number of * milliseconds since midnight January 1, 1970 GMT, and marked by the time the container * received the request. Or <code>-1</code> for client side sessions. * * <p> * Actions that your application takes, such as getting or setting a value associated with the * session, do not affect the access time. * </p> * * @return Last time the client sent a request. Or <code>-1</code> for client side sessions. */ long accessedAt(); /** * The time when this session is going to expire, measured in milliseconds since midnight * January 1, 1970 GMT. Or <code>-1</code> for client side sessions. * * @return The time when this session is going to expire, measured in milliseconds since midnight * January 1, 1970 GMT. Or <code>-1</code> for client side sessions. */ long expiryAt(); /** * Get a object from this session. If the object isn't found this method returns an empty * optional. * * @param name Attribute's name. * @return Value as mutant. */ Mutant get(final String name); /** * @return An immutable copy of local attributes. */ Map<String, String> attributes(); /** * Test if the var name exists inside the session local attributes. * * @param name A local var's name. * @return True, for existing locals. */ boolean isSet(final String name); /** * Set a session local using a the given name. If a local already exists, it will be replaced * with the new value. Keep in mind that null values are NOT allowed. * * @param name Attribute's name. * @param value Attribute's value. * @return This session. */ default Session set(final String name, final byte value) { return set(name, Byte.toString(value)); } /** * Set a session local using a the given name. If a local already exists, it will be replaced * with the new value. Keep in mind that null values are NOT allowed. * * @param name Attribute's name. * @param value Attribute's value. * @return This session. */ default Session set(final String name, final char value) { return set(name, Character.toString(value)); } /** * Set a session local using a the given name. If a local already exists, it will be replaced * with the new value. Keep in mind that null values are NOT allowed. * * @param name Attribute's name. * @param value Attribute's value. * @return This session. */ default Session set(final String name, final boolean value) { return set(name, Boolean.toString(value)); } /** * Set a session local using a the given name. If a local already exists, it will be replaced * with the new value. Keep in mind that null values are NOT allowed. * * @param name Attribute's name. * @param value Attribute's value. * @return This session. */ default Session set(final String name, final short value) { return set(name, Short.toString(value)); } /** * Set a session local using a the given name. If a local already exists, it will be replaced * with the new value. Keep in mind that null values are NOT allowed. * * @param name Attribute's name. * @param value Attribute's value. * @return This session. */ default Session set(final String name, final int value) { return set(name, Integer.toString(value)); } /** * Set a session local using a the given name. If a local already exists, it will be replaced * with the new value. Keep in mind that null values are NOT allowed. * * @param name Attribute's name. * @param value Attribute's value. * @return This session. */ default Session set(final String name, final long value) { return set(name, Long.toString(value)); } /** * Set a session local using a the given name. If a local already exists, it will be replaced * with the new value. Keep in mind that null values are NOT allowed. * * @param name Attribute's name. * @param value Attribute's value. * @return This session. */ default Session set(final String name, final float value) { return set(name, Float.toString(value)); } /** * Set a session local using a the given name. If a local already exists, it will be replaced * with the new value. Keep in mind that null values are NOT allowed. * * @param name Attribute's name. * @param value Attribute's value. * @return This session. */ default Session set(final String name, final double value) { return set(name, Double.toString(value)); } /** * Set a session local using a the given name. If a local already exists, it will be replaced * with the new value. Keep in mind that null values are NOT allowed. * * @param name Attribute's name. * @param value Attribute's value. * @return This session. */ default Session set(final String name, final CharSequence value) { return set(name, value.toString()); } /** * Set a session local using a the given name. If a local already exists, it will be replaced * with the new value. Keep in mind that null values are NOT allowed. * * @param name Attribute's name. * @param value Attribute's value. * @return This session. */ Session set(final String name, final String value); /** * Remove a local value (if any) from session locals. * * @param name Attribute's name. * @return Existing value or empty optional. */ Mutant unset(final String name); /** * Unset/remove all the session data. * * @return This session. */ Session unset(); /** * Invalidates this session then unset any objects bound to it. */ void destroy(); }