All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ninja.session.SessionImpl Maven / Gradle / Ivy

There is a newer version: 7.0.0
Show newest version
/**
 * Copyright (C) the original author or authors.
 *
 * Licensed 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 ninja.session;

import com.google.common.collect.ImmutableMap;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import ninja.Context;
import ninja.Cookie;
import ninja.Result;
import ninja.utils.Clock;
import ninja.utils.CookieDataCodec;
import ninja.utils.CookieEncryption;
import ninja.utils.Crypto;
import ninja.utils.NinjaConstant;
import ninja.utils.NinjaProperties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Inject;


public class SessionImpl implements Session {
    
    private final static Logger logger = LoggerFactory.getLogger(SessionImpl.class);

    private final Crypto crypto;
    private final CookieEncryption encryption;
    private final Clock time;

    private Long sessionExpireTimeInMs;
    private final Long defaultSessionExpireTimeInMs;
    private final Boolean sessionSendOnlyIfChanged;
    private final Boolean sessionTransferredOverHttpsOnly;
    private final Boolean sessionHttpOnly;
    private final String applicationCookieDomain;
    private final Map data = new HashMap();

    /** Has cookie been changed => only send new cookie stuff has been changed */
    private boolean sessionDataHasBeenChanged = false;
    
    private final String sessionCookieName;

    @Inject
    public SessionImpl(Crypto crypto, CookieEncryption encryption, NinjaProperties ninjaProperties, Clock clock) {

        this.crypto = crypto;
        this.encryption = encryption;
        this.time = clock;

        // read configuration stuff:
        Integer sessionExpireTimeInSeconds = ninjaProperties
                .getInteger(NinjaConstant.sessionExpireTimeInSeconds);
        if (sessionExpireTimeInSeconds != null) {
            this.defaultSessionExpireTimeInMs = sessionExpireTimeInSeconds * 1000L;
        } else {
            this.defaultSessionExpireTimeInMs = null;
        }

        this.sessionExpireTimeInMs = defaultSessionExpireTimeInMs;

        this.sessionSendOnlyIfChanged = ninjaProperties.getBooleanWithDefault(
                NinjaConstant.sessionSendOnlyIfChanged, true);
        this.sessionTransferredOverHttpsOnly = ninjaProperties
                .getBooleanWithDefault(
                        NinjaConstant.sessionTransferredOverHttpsOnly, true);
        this.sessionHttpOnly = ninjaProperties.getBooleanWithDefault(
                NinjaConstant.sessionHttpOnly, true);

        this.applicationCookieDomain = ninjaProperties
                .get(NinjaConstant.applicationCookieDomain);
        
        String applicationCookiePrefix = ninjaProperties
                .getOrDie(NinjaConstant.applicationCookiePrefix);
        this.sessionCookieName = applicationCookiePrefix + ninja.utils.NinjaConstant.SESSION_SUFFIX;
    }

    @Override
    public void init(Context context) {

        try {

            // get the cookie that contains session information:
            Cookie cookie = context.getCookie(sessionCookieName);

            // check that the cookie is not empty:
            if (cookie != null && cookie.getValue() != null && !cookie.getValue().trim().isEmpty()) {

                String value = cookie.getValue();

                // the first substring until "-" is the sign
                String sign = value.substring(0, value.indexOf("-"));

                // rest from "-" until the end is the payload of the cookie
                String payload = value.substring(value.indexOf("-") + 1);

                // check if payload is valid:
                if (CookieDataCodec.safeEquals(sign, crypto.signHmacSha1(payload))) {
                    payload = encryption.decrypt(payload);
                    CookieDataCodec.decode(data, payload);
                }

                // If an expiry time was set previously use that instead of the
                // default session expire time.
                if (data.containsKey(EXPIRY_TIME_KEY)) {
                    long expiryTime = Long.parseLong(data.get(EXPIRY_TIME_KEY));
                    if (expiryTime >= 0) {
                        sessionExpireTimeInMs = expiryTime;
                    }
                }

                checkExpire();
            }

        } catch (UnsupportedEncodingException unsupportedEncodingException) {
            logger.error("Encoding exception - this must not happen", unsupportedEncodingException);
        }
    }

    protected boolean shouldExpire() {
        if (sessionExpireTimeInMs != null) {
            // Make sure session contains valid timestamp
            if (!data.containsKey(TIMESTAMP_KEY)) {
                return true;
            }

            long timestamp = Long.parseLong(data.get(TIMESTAMP_KEY));

            return (timestamp + sessionExpireTimeInMs < time.currentTimeMillis());
        }

        return false;
    }

    @Override
    public void setExpiryTime(Long expiryTimeMs) {
        if (expiryTimeMs == null) {
            data.remove(EXPIRY_TIME_KEY);

            sessionExpireTimeInMs = defaultSessionExpireTimeInMs;
            sessionDataHasBeenChanged = true;
        } else {
            data.put(EXPIRY_TIME_KEY, "" + expiryTimeMs);

            sessionExpireTimeInMs = expiryTimeMs;
        }

        if (sessionExpireTimeInMs != null) {
            if (!data.containsKey(TIMESTAMP_KEY)) {
                data.put(TIMESTAMP_KEY, "" + time.currentTimeMillis());
            }

            checkExpire();

            sessionDataHasBeenChanged = true;
        }
    }

    private void checkExpire() {
        if (sessionExpireTimeInMs != null) {
            if (shouldExpire()) {
                sessionDataHasBeenChanged = true;
                data.clear();
            } else {
                // Everything's alright => prolong session
                data.put(TIMESTAMP_KEY, "" + time.currentTimeMillis());
            }
        }
    }

    @Override
    public String getId() {
        if (!data.containsKey(ID_KEY)) {
            put(ID_KEY, UUID.randomUUID().toString());
        }

        return get(ID_KEY);
    }

    @Override
    public Map getData() {
        return ImmutableMap.copyOf(data);
    }

    @Override
    public String getAuthenticityToken() {
        if (!data.containsKey(AUTHENTICITY_KEY)) {
            put(AUTHENTICITY_KEY, UUID.randomUUID().toString());
        }
        return get(AUTHENTICITY_KEY);
    }

    @Override
    public void save(Context context) {

        // Don't save the cookie nothing has changed, and if we're not expiring or
        // we are expiring but we're only updating if the session changes
        if (!sessionDataHasBeenChanged
            && (sessionExpireTimeInMs == null || sessionSendOnlyIfChanged)) {
            // Nothing changed and no cookie-expire, consequently send nothing
            // back.
            return;
        }

        if (isEmpty()) {
            // It is empty, but there was a session coming in, therefore clear it
            if (context.hasCookie(sessionCookieName)) {
                Cookie.Builder cookie = createApplicationCookie(
                        sessionCookieName,
                        "",
                        context)
                        .setMaxAge(0);

                context.addCookie(cookie.build());
            }
            return;

        }

        // Make sure it has a timestamp, if it needs one
        if (sessionExpireTimeInMs != null && !data.containsKey(TIMESTAMP_KEY)) {
            data.put(TIMESTAMP_KEY, Long.toString(System.currentTimeMillis()));
        }

        try {
            String sessionData = CookieDataCodec.encode(data);
            // first encrypt data and then generate HMAC from encrypted data
            // http://crypto.stackexchange.com/questions/202/should-we-mac-then-encrypt-or-encrypt-then-mac
            sessionData = encryption.encrypt(sessionData);
            String sign = crypto.signHmacSha1(sessionData);

            Cookie.Builder cookie = createApplicationCookie(
                    sessionCookieName,
                    sign + "-" + sessionData,
                    context);

            if (sessionExpireTimeInMs != null) {
                cookie.setMaxAge((int) (sessionExpireTimeInMs / 1000L));
            }

            context.addCookie(cookie.build());

        } catch (UnsupportedEncodingException unsupportedEncodingException) {
            logger.error("Encoding exception - this must not happen", unsupportedEncodingException);
            throw new RuntimeException(unsupportedEncodingException);
        }

    }

    @Override
    public void put(String key, String value) {

        // make sure key is valid:
        if (key.contains(":")) {
            throw new IllegalArgumentException(
                    "Character ':' is invalid in a session key.");
        }

        sessionDataHasBeenChanged = true;

        if (value == null) {
            remove(key);
        } else {
            data.put(key, value);
        }

    }

    @Override
    public String get(String key) {
        return data.get(key);
    }

    @Override
    public String remove(String key) {
        sessionDataHasBeenChanged = true;
        String result = get(key);
        data.remove(key);
        return result;
    }

    @Override
    public void clear() {
        sessionDataHasBeenChanged = true;
        data.clear();
    }

    @Override
    public boolean isEmpty() {
        int itemsToIgnore = 0;
        if (data.containsKey(TIMESTAMP_KEY)) {
            itemsToIgnore++;
        }
        if (data.containsKey(EXPIRY_TIME_KEY)) {
            itemsToIgnore++;
        }
        return (data.isEmpty() || data.size() == itemsToIgnore);
    }

    private Cookie.Builder createApplicationCookie(
            String sessionCookieName,
            String value,
            Context context) {
        Cookie.Builder cookie = Cookie.builder(sessionCookieName, value);
        cookie.setPath(context.getContextPath() + "/");

        if (applicationCookieDomain != null) {
            cookie.setDomain(applicationCookieDomain);
        }

        if (sessionTransferredOverHttpsOnly != null) {
            cookie.setSecure(sessionTransferredOverHttpsOnly);
        }

        if (sessionHttpOnly != null) {
            cookie.setHttpOnly(sessionHttpOnly);
        }

        return cookie;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy