blob: e5ebaeb7b307bdc6d0ed6d489566b091c250824a [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.chat;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import org.joda.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.v.v23.V;
import io.v.v23.context.VContext;
import io.v.v23.namespace.Namespace;
import io.v.v23.naming.GlobReply;
import io.v.v23.naming.MountEntry;
import io.v.v23.naming.MountedServer;
import io.v.v23.rpc.Server;
import io.v.v23.rpc.ServerCall;
import io.v.v23.security.BlessingPattern;
import io.v.v23.security.Blessings;
import io.v.v23.security.VPrincipal;
import io.v.v23.security.VSecurity;
import io.v.v23.security.access.AccessList;
import io.v.v23.security.access.Permissions;
import io.v.v23.services.mounttable.Constants;
import io.v.v23.verror.VException;
import io.v.x.chat.vdl.ChatServer;
public class ChatChannel {
private final String name;
private VContext ctx;
private final ChatChannelListener listener;
private static final int MAX_NAME_RETRIES = 25;
private static final Logger logger = Logger.getLogger(ChatChannel.class.getName());
private Server server;
public ChatChannel(ScheduledExecutorService service, VContext ctx, String name,
ChatChannelListener listener) {
this.ctx = ctx;
this.name = name;
this.listener = listener;
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
doUpdateParticipants();
} catch (VException e) {
System.err.println("Could not update the list of chat participants");
e.printStackTrace();
}
}
}, 0, 5, TimeUnit.SECONDS);
}
public void join() throws VException {
final ChatServer chatServer = new ChatServer() {
@Override
public void sendMessage(VContext ctx, ServerCall call, String text) throws VException {
String[] blessingNames = VSecurity.getRemoteBlessingNames(ctx, call.security());
String whom;
if (blessingNames == null || blessingNames.length == 0) {
whom = "unknown";
} else {
whom = blessingUsername(blessingNames[0]);
}
listener.messageReceived(whom, text);
}
};
String mountPath = getLockedPath(ctx);
if (mountPath != null) {
ctx = V.withNewServer(ctx, mountPath, chatServer,
VSecurity.newAllowEveryoneAuthorizer());
} else {
throw new VException("Could not find an appropriate path name for the chat server");
}
}
/**
* Returns a path to which a chat server may be bound, or {@code null} if no appropriate path
* could be found.
*/
private String getLockedPath(VContext ctx) {
VPrincipal principal = V.getPrincipal(ctx);
Blessings defaultBlessings = principal.blessingStore().defaultBlessings();
List<BlessingPattern> patterns = new ArrayList<>();
for (String blessing : VSecurity.getBlessingNames(principal, defaultBlessings)) {
patterns.add(new BlessingPattern(blessing));
}
AccessList myAcl = new AccessList(patterns, ImmutableList.<String>of());
AccessList openAcl = new AccessList(ImmutableList.of(new BlessingPattern("...")),
ImmutableList.<String>of());
Permissions perms = new Permissions();
perms.put(Constants.ADMIN.getValue(), myAcl);
perms.put(Constants.CREATE.getValue(), myAcl);
perms.put(Constants.MOUNT.getValue(), myAcl);
// Let anybody read and resolve the name.
perms.put(Constants.RESOLVE.getValue(), openAcl);
perms.put(Constants.READ.getValue(), openAcl);
for (int i = 0; i < MAX_NAME_RETRIES; i++) {
String path = name + "/" + UUID.randomUUID().toString();
try {
V.getNamespace(ctx).setPermissions(ctx, path, perms, "");
return path;
} catch (VException e) {
logger.log(Level.WARNING, "retry #" + (i + 1) + " failed", e);
// It failed, try another name!
}
}
return null;
}
public void leave() throws VException {
Preconditions.checkState(server != null, "can't leave a channel you haven't joined");
server.stop();
}
public void sendMessage(String s) throws VException {
VContext context = ctx.withTimeout(new Duration(3000));
for (Participant participant : getParticipants()) {
try {
participant.sendMessage(context, s);
} catch (VException e) {
System.err.println("Could not send message to " + participant);
e.printStackTrace();
}
}
}
private void doUpdateParticipants() throws VException {
listener.participantsUpdated(getParticipants());
}
public List<Participant> getParticipants() throws VException {
List<Participant> participants = new ArrayList<>();
VContext context = ctx.withTimeout(new Duration(3000));
Namespace namespace = V.getNamespace(context);
for (GlobReply reply : namespace.glob(context, name + "/*")) {
if (reply instanceof GlobReply.Entry) {
MountEntry entry = ((GlobReply.Entry) reply).getElem();
for (MountedServer server : entry.getServers()) {
participants.add(new Participant(endpointUsername(server.getServer()),
server.getServer()));
break;
}
}
}
return participants;
}
private static String blessingUsername(String blessingName) {
for (String field : Splitter.on("/").split(blessingName)) {
if (field.contains("@")) {
return field;
}
}
return blessingName;
}
private static String endpointUsername(String endpoint) {
// TODO(sjr): replace this with Endpoint once that is written.
Matcher matcher = Pattern.compile("@(.*)@@$").matcher(endpoint);
if (!matcher.matches()) {
return endpoint;
}
List<String> fields = Splitter.on('@').splitToList(matcher.group(1));
if (fields.size() < 5) {
return endpoint;
}
String joinedBlessings = Joiner.on('@').join(fields.subList(5, fields.size()));
for (String blessing : Splitter.on('/').split(joinedBlessings)) {
if (blessing.contains("@")) {
return blessing;
}
}
return endpoint;
}
@Override
public String toString() {
return name;
}
}