/**
* 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.waveprotocol.box.server.authentication;
import java.io.Serializable;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.waveprotocol.box.server.account.AccountData;
import org.waveprotocol.box.server.account.HumanAccountDataImpl;
import org.waveprotocol.box.server.persistence.AccountStore;
import org.waveprotocol.box.server.persistence.PersistenceException;
import org.waveprotocol.box.server.rpc.TransientSessionFilter;
import org.waveprotocol.box.server.rpc.WindowIdFilter;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.util.escapers.PercentEscaper;
import org.waveprotocol.wave.util.logging.Log;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.inject.Inject;
/**
* Utility class for managing the session's authentication status.
*
* It generates {@link HttpWindowSession} instances for the {@link HttpSession}
* interface.
*
* @author josephg@gmail.com (Joseph Gentle)
* @author pablojan@gmail.com (Pablo Ojanguren)
*/
public final class SessionManagerImpl implements SessionManager {
/**
* Use to keep backwards compatibility
*/
private static final String OLD_USER_ID_ATTR = SessionManager.USER_FIELD;
private final AccountStore accountStore;
private final org.eclipse.jetty.server.SessionManager jettySessionManager;
private static final Log LOG = Log.get(SessionManagerImpl.class);
// miliseconds
private static final long USER_SESSION_LIFETIME = 1000L * 60L * 60L * 24L * 30L; // 30 days
//
// Multiple user sessions per http session management
//
public static class SessionUser implements Serializable {
private static final long serialVersionUID = 1L;
public ParticipantId participanId;
public long lastLoginTime;
public String transientSessionId;
public String browserWindowId;
public boolean rememberMe;
public int index;
private String properties = "";
public SessionUser(ParticipantId participanId, long lastLoginTime, String transientSessionid, String browserWindowId,
boolean rememberMe) {
super();
this.participanId = participanId;
this.lastLoginTime = lastLoginTime;
this.transientSessionId = transientSessionid;
this.browserWindowId = browserWindowId;
this.rememberMe = rememberMe;
}
private void propertyMapToString(Map<String, String> m) {
String s = "";
for (Entry<String, String> e: m.entrySet()) {
s+= e.getKey()+"="+e.getValue().replace(";", "") +";";
}
properties = s;
}
private Map<String, String> propertyStringToMap() {
Map<String,String> map = new HashMap<String, String>();
if (properties.isEmpty()) {
return map;
}
String[] propertyArray = properties.split(";");
for (String s: propertyArray) {
if (s != null) {
String[] keyValue = s.split("=");
map.put(keyValue[0], keyValue[1]);
}
}
return map;
}
public void setProperty(String key, String value) {
Map<String, String> propertyMap = propertyStringToMap();
propertyMap.put(key, value);
propertyMapToString(propertyMap);
}
public String getProperty(String key) {
Map<String, String> propertyMap = propertyStringToMap();
return propertyMap.get(key);
}
public Map<String, String> getProperties() {
return propertyStringToMap();
}
public void setProperties(Map<String, String> properties) {
propertyMapToString(properties);
}
}
protected static SessionUser getSessionUser(HttpSession session, ParticipantId participantId) {
return (SessionUser) session.getAttribute(participantId.getAddress());
}
protected static Map<ParticipantId, SessionUser> getSessionUsers(HttpSession session) {
Map<ParticipantId, SessionUser> map = new HashMap<ParticipantId, SessionUser>();
Enumeration<String> names = session.getAttributeNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
if (!name.equals(OLD_USER_ID_ATTR))
map.put(ParticipantId.ofUnsafe(name), (SessionUser) session.getAttribute(name));
}
return map;
}
protected static Map<Integer, ParticipantId> getSessionUserIndex(HttpSession session) {
Map<Integer, ParticipantId> map = new HashMap<Integer, ParticipantId>();
Enumeration<String> names = session.getAttributeNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
if (!name.equals(OLD_USER_ID_ATTR)) {
SessionUser su = (SessionUser) session.getAttribute(name);
map.put(su.index, su.participanId);
}
}
return map;
}
protected static int addSessionUser(HttpSession session, SessionUser sessionUser) {
Map<ParticipantId, SessionUser> sessionUsers = getSessionUsers(session);
Map<Integer, ParticipantId> sessionUserIndex = getSessionUserIndex(session);
if (!sessionUsers.containsKey(sessionUser.participanId)) {
int index = 0;
while (index < sessionUsers.size()) {
if (!sessionUserIndex.containsKey(index)) {
break;
}
index++;
}
sessionUser.index = index;
} else {
sessionUser.index = sessionUsers.get(sessionUser.participanId).index;
}
// Rewrite
session.setAttribute(sessionUser.participanId.getAddress(), sessionUser);
return sessionUser.index;
}
protected static boolean removeSessionUser(HttpSession session, ParticipantId participantId) {
boolean exists = session.getAttribute(participantId.getAddress()) != null;
session.removeAttribute(participantId.getAddress());
return exists;
}
@Inject
public SessionManagerImpl(
AccountStore accountStore, org.eclipse.jetty.server.SessionManager jettySessionManager) {
Preconditions.checkNotNull(accountStore, "Null account store");
Preconditions.checkNotNull(jettySessionManager, "Null jetty session manager");
this.accountStore = accountStore;
this.jettySessionManager = jettySessionManager;
}
//
// Old Wave methods
//
@Override
public void login(HttpSession session, ParticipantId id) {
Preconditions.checkNotNull(session, "Session is null");
Preconditions.checkNotNull(id, "Participant id is null");
session.setAttribute(OLD_USER_ID_ATTR, id);
}
@Override
public boolean logout(HttpSession session) {
Preconditions.checkNotNull(session, "Session is null");
boolean exists = session.getAttribute(OLD_USER_ID_ATTR) != null;
if (exists)
session.removeAttribute(OLD_USER_ID_ATTR);
return exists;
}
@Override
public ParticipantId getLoggedInUser(HttpSession session) {
return session != null ? (ParticipantId) session.getAttribute(OLD_USER_ID_ATTR) : null;
}
@Override
public AccountData getLoggedInAccount(HttpServletRequest request) {
// Consider caching the account data in the session object.
ParticipantId user = getLoggedInUser(request);
return getAccountData(user);
}
@Override
public AccountData getAccountData(ParticipantId user) {
if (user != null) {
if (user.isAnonymous()) {
// Set up a fake humman account for anonymous users
return new HumanAccountDataImpl(user);
}
try {
return accountStore.getAccount(user);
} catch (PersistenceException e) {
LOG.warning("Failed to retrieve account data for " + user, e);
return null;
}
} else {
return null;
}
}
@Override
public AccountData getLoggedInAccount(HttpSession session) {
ParticipantId participandId = getLoggedInUser(session);
return getAccountData(participandId);
}
@Override
public String getLoginUrl(String redirect) {
if (Strings.isNullOrEmpty(redirect)) {
return SIGN_IN_URL;
} else {
PercentEscaper escaper =
new PercentEscaper(PercentEscaper.SAFEQUERYSTRINGCHARS_URLENCODER, false);
String queryStr = "?r=" + escaper.escape(redirect);
return SIGN_IN_URL + queryStr;
}
}
//
// New SwellRT methdods
//
@Override
public ParticipantId getLoggedInUser(HttpServletRequest request) {
HttpSession session = request.getSession();
if (session == null)
return null;
String transientSessionId = getTransientSessionId(request);
String browserWindowId = getBrowserWindowId(request);
return getLoggedInUser(session, transientSessionId, browserWindowId);
}
protected ParticipantId getLoggedInUser(HttpSession session, String transientSessionId, String browserWindowId) {
SessionUser loggedSessionUser = null;
for (SessionUser su: getSessionUsers(session).values()) {
if (su.browserWindowId.equals(browserWindowId) && su.transientSessionId.equals(transientSessionId)) {
if (loggedSessionUser == null) {
loggedSessionUser = su;
} else if (su.lastLoginTime > loggedSessionUser.lastLoginTime) {
// pick the most recent login
loggedSessionUser = su;
}
}
}
return loggedSessionUser != null ? loggedSessionUser.participanId : null;
}
@Override
public Set<ParticipantId> getAllLoggedInUser(HttpServletRequest request) {
final Set<ParticipantId> participants = new HashSet<ParticipantId>();
HttpSession session = request.getSession(false);
if (session == null)
return participants;
getSessionUsers(session).values().forEach(su -> {
if (isLoginSessionUser(request, su)) {
participants.add(su.participanId);
}
});
return participants;
}
@Override
public ParticipantId getLoggedInUser(String token) {
Preconditions.checkNotNull(token, "Token is null");
String[] parts = token.split(":");
Preconditions.checkArgument(parts.length == 3);
String permanentSessionId = parts[0];
String transientSessionId = parts[1];
String browserWindowId = parts[2];
HttpSession session = jettySessionManager.getHttpSession(permanentSessionId);
return getLoggedInUser(session, transientSessionId, browserWindowId);
}
@Override
public int login(HttpServletRequest request, ParticipantId participantId, boolean rememberMe) {
Preconditions.checkNotNull(request, "Request is null");
Preconditions.checkNotNull(participantId, "Participant id is null");
HttpSession session = request.getSession(true);
// Remember always session data
SessionUser su = getSessionUser(session, participantId);
if (su != null) {
updateSessionUser(request, su);
return su.index;
} else {
SessionUser userRecord = new SessionUser(participantId, System.currentTimeMillis(), getTransientSessionId(request), getBrowserWindowId(request), rememberMe);
return addSessionUser(session, userRecord);
}
}
protected boolean isLoginSessionUser(HttpServletRequest request, SessionUser su) {
String transientSessionId = getTransientSessionId(request);
String browserWindowId = getBrowserWindowId(request);
return (su.browserWindowId.equals(browserWindowId) && su.transientSessionId.equals(transientSessionId));
}
protected boolean canResumeSessionUser(HttpServletRequest request, SessionUser su) {
if (su.participanId.isAnonymous())
return false;
if (su.rememberMe) {
// Remember-me sessions are valid until the defined expiration time
long nowTime = System.currentTimeMillis();
return nowTime - su.lastLoginTime < USER_SESSION_LIFETIME;
} else {
// Normal sessions are valid only during a browser session
return (su.transientSessionId != null) && su.transientSessionId.equals(getTransientSessionId(request));
}
}
protected void updateSessionUser(HttpServletRequest request, SessionUser su) {
su.lastLoginTime = System.currentTimeMillis();
su.browserWindowId = getBrowserWindowId(request);
su.transientSessionId = getTransientSessionId(request);
addSessionUser(request.getSession(), su);
}
@Override
public ParticipantId resume(HttpServletRequest request, Integer userIndex) {
Preconditions.checkNotNull(request, "Request is null");
HttpSession session = request.getSession(true);
Map<ParticipantId, SessionUser> sessionUserMap = getSessionUsers(session);
Map<Integer, ParticipantId> sessionUserIndex = getSessionUserIndex(session);
if (userIndex == null || !sessionUserIndex.containsKey(userIndex)) {
// Return first valid user session
ParticipantId resumeAs = null;
for (ParticipantId p: sessionUserMap.keySet()) {
SessionUser su = sessionUserMap.get(p);
if (canResumeSessionUser(request, su)) {
updateSessionUser(request, su);
resumeAs = p;
break;
}
}
return resumeAs;
} else {
ParticipantId resumeAs = sessionUserIndex.get(userIndex);
SessionUser sessionUser = sessionUserMap.get(resumeAs);
if (canResumeSessionUser(request, sessionUser)) {
updateSessionUser(request, sessionUser);
return resumeAs;
}
}
return null;
}
@Override
public boolean logout(HttpServletRequest request, ParticipantId participantId) {
Preconditions.checkNotNull(request, "Request is null");
Preconditions.checkNotNull(participantId, "Participant is null");
HttpSession session = request.getSession(false);
return session != null ? removeSessionUser(session, participantId) : false;
}
@Override
public Map<Integer, ParticipantId> getSessionUsersIndex(HttpServletRequest request) {
HttpSession session = request.getSession(false);
Map<Integer, ParticipantId> sessionUserIndex = session != null ? getSessionUserIndex(session) : Collections.emptyMap();
return sessionUserIndex;
}
@Override
public Cookie getTransientSessionCookie(HttpServletRequest request) {
return getCookie(request, SessionManager.TRASIENT_SESSION_COOKIE_NAME); // TODO replace with config prop
}
@Override
public Cookie getSessionCookie(HttpServletRequest request) {
return getCookie(request, SessionManager.SESSION_COOKIE_NAME); // TODO replace with config prop
}
@Override
public String getTransientSessionId(HttpServletRequest request) {
return (String) request.getAttribute(TransientSessionFilter.REQUEST_ATTR_TSESSION_ID);
}
public String getBrowserWindowId(HttpServletRequest request) {
return (String) request.getAttribute(WindowIdFilter.REQUEST_ATTR_WINDOW_ID);
}
@Override
public String getSessionId(HttpServletRequest request) {
return request.getSession().getId();
}
@Override
public Map<String, String> getSessionProperties(HttpServletRequest request) {
ParticipantId participantId = getLoggedInUser(request);
return getSessionUser(request.getSession(), participantId).getProperties();
}
@Override
public void setSessionProperties(HttpServletRequest request, Map<String, String> properties) {
ParticipantId participantId = getLoggedInUser(request);
getSessionUser(request.getSession(), participantId).setProperties(properties);
}
protected static Cookie getCookie(HttpServletRequest request, String name) {
Preconditions.checkNotNull(request, "Request can't be null");
Preconditions.checkNotNull(name, "Cookie name can't be null");
Cookie[] cookies = request.getCookies();
if (cookies != null)
for (Cookie c: cookies) {
if (c.getName().equalsIgnoreCase(name))
return c;
}
return null;
}
}