// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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 com.google.collide.server.participants;
import com.google.collide.dto.server.DtoServerImpls.GetWorkspaceParticipantsResponseImpl;
import com.google.collide.dto.server.DtoServerImpls.ParticipantImpl;
import com.google.collide.dto.server.DtoServerImpls.ParticipantUserDetailsImpl;
import com.google.collide.dto.server.DtoServerImpls.UserDetailsImpl;
import com.google.collide.server.shared.util.Dto;
import org.vertx.java.busmods.BusModBase;
import org.vertx.java.core.Handler;
import org.vertx.java.core.eventbus.Message;
import org.vertx.java.core.json.JsonObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
/**
* Acts as the authentication provider, with a compatible API subset with the bundled default
* AuthManager that comes with Vertx.
*
* This one however is in-memory, and also has affordances for broadcasting to joined participants.
* Also, this implementation allows for a single username to be logged in as multiple different
* sessions.
*/
public class Participants extends BusModBase {
public static final String CLIENT_ADDRESS_PREFX = "client";
public static final String PAYLOAD_TAG = "payload";
public static final String OMIT_SENDER_TAG = "omitSender";
public static final String TARGET_SPECIFIC_CLIENT_TAG = "sendToClient";
public static final String TARGET_USERS_TABS_TAG = "sendToUsersTabs";
private static final long DEFAULT_LOGIN_TIMEOUT = 60 * 60 * 1000; // 1 hour
// TODO: This is temporarily set to 30 mins for testing.
private static final long DEFAULT_KEEP_ALIVE_TIMEOUT = 30 * 1000 * 60; // 30 secs
/**
* A single Collide tab for a logged in user that is connected to the eventbus.
*/
private static final class ConnectedTab {
final LoggedInUser loginInfo;
long timerId;
ConnectedTab(LoggedInUser loginInfo, long tabDisconnectTimerId) {
this.loginInfo = loginInfo;
this.timerId = tabDisconnectTimerId;
}
}
/**
* A logged in user that may have multiple tabs open.
*/
private static final class LoggedInUser {
final String username;
/** Stable user ID for the lifetime of the server. */
final String userId;
long timerId;
private LoggedInUser(String username) {
this.username = username;
this.userId = getStableUserId(username);
}
/** Stable identifier for a username. Stable for the lifetime of the server. */
static final Map<String, String> usernameToStableIdMap = new HashMap<String, String>();
static String getStableUserId(String username) {
String stableId = usernameToStableIdMap.get(username);
if (stableId == null) {
stableId = UUID.randomUUID().toString();
usernameToStableIdMap.put(username, stableId);
}
return stableId;
}
}
private String password;
private long tabKeepAliveTimeout;
private long loginSessionTimeout;
/** Map of per-tab active client IDs to ConnectedTabs. */
protected final Map<String, ConnectedTab> connectedTabs = new HashMap<String, ConnectedTab>();
/** Map of per-user session IDs LoggedInUsers. */
protected final Map<String, LoggedInUser> loggedInUsers = new HashMap<String, LoggedInUser>();
@Override
public void start() {
super.start();
this.password = getOptionalStringConfig("password", "");
this.loginSessionTimeout = getOptionalLong("session_timeout", DEFAULT_LOGIN_TIMEOUT);
this.tabKeepAliveTimeout = getOptionalLong("keep_alive_timeout", DEFAULT_KEEP_ALIVE_TIMEOUT);
String addressBase = getOptionalStringConfig("address", "participants");
eb.registerHandler(addressBase + ".login", new Handler<Message<JsonObject>>() {
@Override
public void handle(Message<JsonObject> message) {
doLogin(message);
}
});
eb.registerHandler(addressBase + ".logout", new Handler<Message<JsonObject>>() {
@Override
public void handle(Message<JsonObject> message) {
doLogout(message);
}
});
eb.registerHandler(addressBase + ".authorise", new Handler<Message<JsonObject>>() {
@Override
public void handle(Message<JsonObject> message) {
doAuthorise(message);
}
});
eb.registerHandler(addressBase + ".keepAlive", new Handler<Message<JsonObject>>() {
@Override
public void handle(Message<JsonObject> event) {
doKeepAlive(event);
}
});
eb.registerHandler(addressBase + ".getParticipants", new Handler<Message<JsonObject>>() {
@Override
public void handle(Message<JsonObject> event) {
doGetParticipants(event);
}
});
eb.registerHandler(addressBase + ".broadcast", new Handler<Message<JsonObject>>() {
@Override
public void handle(Message<JsonObject> event) {
doBroadcast(event);
}
});
eb.registerHandler(addressBase + ".sendTo", new Handler<Message<JsonObject>>() {
@Override
public void handle(Message<JsonObject> event) {
doSendTo(event);
}
});
}
private long getOptionalLong(String fieldName, long defaultVal) {
Number val = config.getNumber(fieldName);
if (val == null) {
return defaultVal;
}
return val instanceof Integer ? (Integer) val : (Long) val;
}
void doBroadcast(Message<JsonObject> event) {
String payload = event.body.getString(PAYLOAD_TAG);
String senderActiveClientId = event.body.getString(OMIT_SENDER_TAG);
Set<Entry<String, ConnectedTab>> entries = connectedTabs.entrySet();
for (Entry<String, ConnectedTab> entry : entries) {
String activeClientId = entry.getKey();
String address = CLIENT_ADDRESS_PREFX + "." + activeClientId;
// Send to everyone except the optionally specified sender that we wish to ignore.
if (!activeClientId.equals(senderActiveClientId)) {
vertx.eventBus().send(address, Dto.wrap(payload));
}
}
}
void doSendTo(Message<JsonObject> event) {
String payload = event.body.getString(PAYLOAD_TAG);
List<String> clientsToMessage = new ArrayList<String>();
String activeClientId = event.body.getString(TARGET_SPECIFIC_CLIENT_TAG);
if (activeClientId != null) {
// Send to a specific tab.
ConnectedTab tab = connectedTabs.get(activeClientId);
if (tab != null) {
clientsToMessage.add(activeClientId);
}
} else {
String username = event.body.getString(TARGET_USERS_TABS_TAG);
if (username != null) {
// Collect the ids of all this user's open tabs.
Set<Entry<String, ConnectedTab>> allClients = connectedTabs.entrySet();
for (Entry<String, ConnectedTab> entry : allClients) {
if (username.equals(entry.getValue().loginInfo.username)) {
clientsToMessage.add(entry.getKey());
}
}
}
}
// Message the clients.
for (String cid : clientsToMessage) {
vertx.eventBus().send(CLIENT_ADDRESS_PREFX + "." + cid, Dto.wrap(payload));
}
}
/**
* Returns all the connected tabs, as well as the user information for the user that owns each
* tab.
*/
void doGetParticipants(Message<JsonObject> event) {
GetWorkspaceParticipantsResponseImpl resp = GetWorkspaceParticipantsResponseImpl.make();
List<ParticipantUserDetailsImpl> collaboratorsArr = new ArrayList<ParticipantUserDetailsImpl>();
Set<Entry<String, ConnectedTab>> collaborators = connectedTabs.entrySet();
for (Entry<String, ConnectedTab> entry : collaborators) {
String userId = entry.getValue().loginInfo.userId;
String username = entry.getValue().loginInfo.username;
ParticipantUserDetailsImpl participantDetails = ParticipantUserDetailsImpl.make();
ParticipantImpl participant = ParticipantImpl.make().setId(entry.getKey()).setUserId(userId);
UserDetailsImpl userDetails = UserDetailsImpl.make()
.setUserId(userId).setDisplayEmail(username).setDisplayName(username)
.setGivenName(username);
participantDetails.setParticipant(participant);
participantDetails.setUserDetails(userDetails);
collaboratorsArr.add(participantDetails);
}
resp.setParticipants(collaboratorsArr);
event.reply(Dto.wrap(resp));
}
void doKeepAlive(Message<JsonObject> event) {
final String activeClientId = event.body.getString("activeClient");
if (activeClientId != null) {
ConnectedTab loginInfo = connectedTabs.get(activeClientId);
if (loginInfo != null) {
vertx.cancelTimer(loginInfo.timerId);
loginInfo.timerId = vertx.setTimer(tabKeepAliveTimeout, new Handler<Long>() {
@Override
public void handle(Long timerID) {
connectedTabs.remove(activeClientId);
}
});
}
}
}
void doLogin(final Message<JsonObject> message) {
final String username = message.body.getString("username", null);
if (username == null) {
sendStatus("denied", message);
return;
}
String password = message.body.getString("password", null);
if (password == null && !"".equals(this.password)) {
sendStatus("denied", message, new JsonObject().putString("reason", "needs-pass"));
return;
}
if(!authenticate(username, password)) {
sendStatus("denied", message);
return;
}
// Passed authentication. Create a logged in user and a timer to expire his session.
if (alreadyLoggedIn(username)) {
// Cancel the previous session logout timer.
LoggedInUser existing = loggedInUsers.remove(LoggedInUser.getStableUserId(username));
vertx.cancelTimer(existing.timerId);
}
final LoggedInUser user = new LoggedInUser(username);
loggedInUsers.put(user.userId, user);
user.timerId = vertx.setTimer(loginSessionTimeout, new Handler<Long>() {
@Override
public void handle(Long event) {
logout(user.userId);
}
});
// The spelling "sessionID" is needed to work with the vertx eventbus bridge whitelist.
JsonObject jsonReply = new JsonObject().putString("sessionID", user.userId);
sendOK(message, jsonReply);
}
/**
* This is NOT the same as authenticating. This just checks to see if we are tracking a login
* session for this username already.
*/
private boolean alreadyLoggedIn(String username) {
return loggedInUsers.get(LoggedInUser.getStableUserId(username)) != null;
}
private String createActiveTab(LoggedInUser user) {
final String activeClient = UUID.randomUUID().toString();
long timerId = vertx.setTimer(tabKeepAliveTimeout, new Handler<Long>() {
@Override
public void handle(Long timerId) {
connectedTabs.remove(activeClient);
}
});
connectedTabs.put(activeClient, new ConnectedTab(user, timerId));
return activeClient;
}
private boolean authenticate(String username, String password) {
return "".equals(this.password) || password.equals(this.password);
}
void doLogout(final Message<JsonObject> message) {
final String sessionID = getMandatoryString("sessionID", message);
if (sessionID != null) {
if (logout(sessionID)) {
sendOK(message);
} else {
super.sendError(message, "Not logged in");
}
}
}
private boolean logout(String userId) {
LoggedInUser user = loggedInUsers.remove(userId);
if (user != null) {
List<Entry<String, ConnectedTab>> usersTabs = new ArrayList<Entry<String, ConnectedTab>>();
Set<Entry<String, ConnectedTab>> entries = connectedTabs.entrySet();
for (Entry<String, ConnectedTab> entry : entries) {
if (userId.equals(entry.getValue().loginInfo.userId)) {
usersTabs.add(entry);
}
}
for (int i=0;i<usersTabs.size();i++) {
connectedTabs.remove(usersTabs.get(i).getKey());
vertx.cancelTimer(usersTabs.get(i).getValue().timerId);
}
return true;
} else {
return false;
}
}
void doAuthorise(Message<JsonObject> message) {
String userId = getMandatoryString("sessionID", message);
if (userId == null) {
sendStatus("denied", message);
return;
}
LoggedInUser user = loggedInUsers.get(userId);
if (user != null || "".equals(password)) {
String username = message.body.getString("username", "anonymous");
if (user != null) {
username = user.username;
} else {
user = new LoggedInUser(username);
}
JsonObject reply = new JsonObject().putString("username", username);
if (message.body.getBoolean("createClient", false)) {
String activeClient = createActiveTab(user);
reply.putString("activeClient", activeClient);
}
sendOK(message, reply);
} else {
sendStatus("denied", message);
}
}
}