From 5bd90b0452918b8e561188b1511be541c375dc57 Mon Sep 17 00:00:00 2001 From: Anton Keks Date: Wed, 23 Aug 2017 18:01:32 +0300 Subject: [PATCH 1/4] Extract SessionStore interface with its default implementation CookieSessionStore This will allow adding of other session stores, e.g. cache or database. --- .../src/play/mvc/CookieSessionStore.java | 96 +++++++++++++++++++ framework/src/play/mvc/Scope.java | 89 ++--------------- framework/src/play/mvc/SessionStore.java | 9 ++ 3 files changed, 112 insertions(+), 82 deletions(-) create mode 100644 framework/src/play/mvc/CookieSessionStore.java create mode 100644 framework/src/play/mvc/SessionStore.java diff --git a/framework/src/play/mvc/CookieSessionStore.java b/framework/src/play/mvc/CookieSessionStore.java new file mode 100644 index 0000000000..fc302984d6 --- /dev/null +++ b/framework/src/play/mvc/CookieSessionStore.java @@ -0,0 +1,96 @@ +package play.mvc; + +import play.Play; +import play.exceptions.UnexpectedException; +import play.libs.Crypto; +import play.libs.Time; + +import static play.mvc.Scope.*; +import static play.mvc.Scope.Session.TS_KEY; + +/** + * Default session store implementation that stores signed data in a cookie + */ +public class CookieSessionStore implements SessionStore { + + @Override + public Session restore() { + try { + Session session = new Session(); + Http.Cookie cookie = Http.Request.current().cookies.get(COOKIE_PREFIX + "_SESSION"); + int duration = Time.parseDuration(COOKIE_EXPIRE); + long expiration = (duration * 1000l); + + if (cookie != null && Play.started && cookie.value != null && !cookie.value.trim().equals("")) { + String value = cookie.value; + int firstDashIndex = value.indexOf("-"); + if (firstDashIndex > -1) { + String sign = value.substring(0, firstDashIndex); + String data = value.substring(firstDashIndex + 1); + if (CookieDataCodec.safeEquals(sign, Crypto.sign(data, Play.secretKey.getBytes()))) { + CookieDataCodec.decode(session.data, data); + } + } + if (COOKIE_EXPIRE != null) { + // Verify that the session contains a timestamp, and + // that it's not expired + if (!session.contains(TS_KEY)) { + session = new Session(); + } else { + if ((Long.parseLong(session.get(TS_KEY))) < System.currentTimeMillis()) { + // Session expired + session = new Session(); + } + } + session.put(TS_KEY, System.currentTimeMillis() + expiration); + } else { + // Just restored. Nothing changed. No cookie-expire. + session.changed = false; + } + } else { + // no previous cookie to restore; but we may have to set the + // timestamp in the new cookie + if (COOKIE_EXPIRE != null) { + session.put(TS_KEY, (System.currentTimeMillis() + expiration)); + } + } + + return session; + } catch (Exception e) { + throw new UnexpectedException("Corrupted HTTP session from " + Http.Request.current().remoteAddress, e); + } + } + + @Override + public void save(Session session) { + if (Http.Response.current() == null) { + // Some request like WebSocket don't have any response + return; + } + if (!session.changed && SESSION_SEND_ONLY_IF_CHANGED && COOKIE_EXPIRE == null) { + // Nothing changed and no cookie-expire, consequently send + // nothing back. + return; + } + if (session.isEmpty()) { + // The session is empty: delete the cookie + if (Http.Request.current().cookies.containsKey(COOKIE_PREFIX + "_SESSION") || !SESSION_SEND_ONLY_IF_CHANGED) { + Http.Response.current().setCookie(COOKIE_PREFIX + "_SESSION", "", null, "/", 0, COOKIE_SECURE, SESSION_HTTPONLY); + } + return; + } + try { + String sessionData = CookieDataCodec.encode(session.data); + String sign = Crypto.sign(sessionData, Play.secretKey.getBytes()); + if (COOKIE_EXPIRE == null) { + Http.Response.current().setCookie(COOKIE_PREFIX + "_SESSION", sign + "-" + sessionData, null, "/", null, COOKIE_SECURE, + SESSION_HTTPONLY); + } else { + Http.Response.current().setCookie(COOKIE_PREFIX + "_SESSION", sign + "-" + sessionData, null, "/", + Time.parseDuration(COOKIE_EXPIRE), COOKIE_SECURE, SESSION_HTTPONLY); + } + } catch (Exception e) { + throw new UnexpectedException("Session serializationProblem", e); + } + } +} diff --git a/framework/src/play/mvc/Scope.java b/framework/src/play/mvc/Scope.java index bf1b5695fc..1ebcd4135f 100644 --- a/framework/src/play/mvc/Scope.java +++ b/framework/src/play/mvc/Scope.java @@ -1,13 +1,5 @@ package play.mvc; -import java.lang.annotation.Annotation; -import java.net.URLEncoder; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.UUID; - import play.Logger; import play.Play; import play.data.binding.Binder; @@ -20,9 +12,12 @@ import play.i18n.Messages; import play.libs.Codec; import play.libs.Crypto; -import play.libs.Time; import play.utils.Utils; +import java.lang.annotation.Annotation; +import java.net.URLEncoder; +import java.util.*; + /** * All application Scopes */ @@ -157,56 +152,14 @@ public String toString() { * Session scope */ public static class Session { + public static SessionStore store = new CookieSessionStore(); static final String AT_KEY = "___AT"; static final String ID_KEY = "___ID"; static final String TS_KEY = "___TS"; public static Session restore() { - try { - Session session = new Session(); - Http.Cookie cookie = Http.Request.current().cookies.get(COOKIE_PREFIX + "_SESSION"); - int duration = Time.parseDuration(COOKIE_EXPIRE); - long expiration = (duration * 1000l); - - if (cookie != null && Play.started && cookie.value != null && !cookie.value.trim().equals("")) { - String value = cookie.value; - int firstDashIndex = value.indexOf("-"); - if (firstDashIndex > -1) { - String sign = value.substring(0, firstDashIndex); - String data = value.substring(firstDashIndex + 1); - if (CookieDataCodec.safeEquals(sign, Crypto.sign(data, Play.secretKey.getBytes()))) { - CookieDataCodec.decode(session.data, data); - } - } - if (COOKIE_EXPIRE != null) { - // Verify that the session contains a timestamp, and - // that it's not expired - if (!session.contains(TS_KEY)) { - session = new Session(); - } else { - if ((Long.parseLong(session.get(TS_KEY))) < System.currentTimeMillis()) { - // Session expired - session = new Session(); - } - } - session.put(TS_KEY, System.currentTimeMillis() + expiration); - } else { - // Just restored. Nothing changed. No cookie-expire. - session.changed = false; - } - } else { - // no previous cookie to restore; but we may have to set the - // timestamp in the new cookie - if (COOKIE_EXPIRE != null) { - session.put(TS_KEY, (System.currentTimeMillis() + expiration)); - } - } - - return session; - } catch (Exception e) { - throw new UnexpectedException("Corrupted HTTP session from " + Http.Request.current().remoteAddress, e); - } + return store.restore(); } Map data = new HashMap<>(); // ThreadLocal access @@ -241,35 +194,7 @@ void change() { } void save() { - if (Http.Response.current() == null) { - // Some request like WebSocket don't have any response - return; - } - if (!changed && SESSION_SEND_ONLY_IF_CHANGED && COOKIE_EXPIRE == null) { - // Nothing changed and no cookie-expire, consequently send - // nothing back. - return; - } - if (isEmpty()) { - // The session is empty: delete the cookie - if (Http.Request.current().cookies.containsKey(COOKIE_PREFIX + "_SESSION") || !SESSION_SEND_ONLY_IF_CHANGED) { - Http.Response.current().setCookie(COOKIE_PREFIX + "_SESSION", "", null, "/", 0, COOKIE_SECURE, SESSION_HTTPONLY); - } - return; - } - try { - String sessionData = CookieDataCodec.encode(data); - String sign = Crypto.sign(sessionData, Play.secretKey.getBytes()); - if (COOKIE_EXPIRE == null) { - Http.Response.current().setCookie(COOKIE_PREFIX + "_SESSION", sign + "-" + sessionData, null, "/", null, COOKIE_SECURE, - SESSION_HTTPONLY); - } else { - Http.Response.current().setCookie(COOKIE_PREFIX + "_SESSION", sign + "-" + sessionData, null, "/", - Time.parseDuration(COOKIE_EXPIRE), COOKIE_SECURE, SESSION_HTTPONLY); - } - } catch (Exception e) { - throw new UnexpectedException("Session serializationProblem", e); - } + store.save(this); } public void put(String key, String value) { diff --git a/framework/src/play/mvc/SessionStore.java b/framework/src/play/mvc/SessionStore.java new file mode 100644 index 0000000000..5bd8eea002 --- /dev/null +++ b/framework/src/play/mvc/SessionStore.java @@ -0,0 +1,9 @@ +package play.mvc; + +/** + * Implementations of session storage mechanisms. + */ +public interface SessionStore { + void save(Scope.Session session); + Scope.Session restore(); +} From 0ed302074aaef8cce9f7407a3e48a3fa99860943 Mon Sep 17 00:00:00 2001 From: Anton Keks Date: Wed, 23 Aug 2017 18:04:30 +0300 Subject: [PATCH 2/4] Use Codec.UUID() as in getId() --- framework/src/play/mvc/Scope.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/play/mvc/Scope.java b/framework/src/play/mvc/Scope.java index 1ebcd4135f..067ab43864 100644 --- a/framework/src/play/mvc/Scope.java +++ b/framework/src/play/mvc/Scope.java @@ -184,7 +184,7 @@ public Map all() { public String getAuthenticityToken() { if (!data.containsKey(AT_KEY)) { - this.put(AT_KEY, Crypto.sign(UUID.randomUUID().toString())); + this.put(AT_KEY, Crypto.sign(Codec.UUID())); } return data.get(AT_KEY); } From a44eb0c85601b3aab69f7f048870477ff021cd2a Mon Sep 17 00:00:00 2001 From: Anton Keks Date: Wed, 23 Aug 2017 18:27:07 +0300 Subject: [PATCH 3/4] SessionStore is now configurable using application.session.store conf parameter. CookieSessionStore is the default. --- framework/src/play/mvc/Scope.java | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/framework/src/play/mvc/Scope.java b/framework/src/play/mvc/Scope.java index 067ab43864..1b882c8a33 100644 --- a/framework/src/play/mvc/Scope.java +++ b/framework/src/play/mvc/Scope.java @@ -18,6 +18,8 @@ import java.net.URLEncoder; import java.util.*; +import static play.templates.JavaExtensions.capitalizeWords; + /** * All application Scopes */ @@ -32,6 +34,19 @@ public class Scope { public static final boolean SESSION_SEND_ONLY_IF_CHANGED = Play.configuration .getProperty("application.session.sendOnlyIfChanged", "false").toLowerCase().equals("true"); + public static SessionStore sessionStore = createSessionStore(); + + private static SessionStore createSessionStore() { + String sessionStoreParameter = Play.configuration.getProperty("application.session.store", "cookie"); + String sessionStoreClass = "play.mvc." + capitalizeWords(sessionStoreParameter) + "SessionStore"; + try { + return (SessionStore) Class.forName(sessionStoreClass).newInstance(); + } + catch (Exception e) { + throw new UnexpectedException("Cannot create instance of " + sessionStoreClass, e); + } + } + /** * Flash scope */ @@ -152,14 +167,13 @@ public String toString() { * Session scope */ public static class Session { - public static SessionStore store = new CookieSessionStore(); static final String AT_KEY = "___AT"; static final String ID_KEY = "___ID"; static final String TS_KEY = "___TS"; public static Session restore() { - return store.restore(); + return sessionStore.restore(); } Map data = new HashMap<>(); // ThreadLocal access @@ -194,7 +208,7 @@ void change() { } void save() { - store.save(this); + sessionStore.save(this); } public void put(String key, String value) { From 9d235842c1f13c18b4acbba17fc453edb5a978a8 Mon Sep 17 00:00:00 2001 From: Anton Keks Date: Thu, 24 Aug 2017 11:13:45 +0300 Subject: [PATCH 4/4] Use application.session.storeClass config parameter instead of class naming conventions --- framework/src/play/mvc/Scope.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/framework/src/play/mvc/Scope.java b/framework/src/play/mvc/Scope.java index 1b882c8a33..6b3c17b1ea 100644 --- a/framework/src/play/mvc/Scope.java +++ b/framework/src/play/mvc/Scope.java @@ -18,8 +18,6 @@ import java.net.URLEncoder; import java.util.*; -import static play.templates.JavaExtensions.capitalizeWords; - /** * All application Scopes */ @@ -37,9 +35,10 @@ public class Scope { public static SessionStore sessionStore = createSessionStore(); private static SessionStore createSessionStore() { - String sessionStoreParameter = Play.configuration.getProperty("application.session.store", "cookie"); - String sessionStoreClass = "play.mvc." + capitalizeWords(sessionStoreParameter) + "SessionStore"; + String sessionStoreClass = Play.configuration.getProperty("application.session.storeClass"); + if (sessionStoreClass == null) return new CookieSessionStore(); try { + Logger.info("Storing sessions using " + sessionStoreClass); return (SessionStore) Class.forName(sessionStoreClass).newInstance(); } catch (Exception e) {