blob: c968ee233b054226c59f0b926b49cef7a98a15bb [file] [log] [blame]
// Copyright 2016 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.android.inspectors;
import android.util.Base64;
import android.util.Log;
import org.joda.time.DateTime;
import org.joda.time.ReadableDuration;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPublicKey;
import java.util.ArrayList;
import java.util.List;
import io.v.android.v23.V;
import io.v.v23.context.VContext;
import io.v.v23.naming.Endpoint;
import io.v.v23.rpc.ListenSpec;
import io.v.v23.rpc.PublisherEntry;
import io.v.v23.rpc.Server;
import io.v.v23.rpc.ServerStatus;
import io.v.v23.security.Blessings;
import io.v.v23.security.Caveat;
import io.v.v23.security.VPrincipal;
import io.v.v23.security.VSecurity;
import io.v.v23.security.access.Constants;
import io.v.v23.security.access.Tag;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;
/**
* A RemoteInspectors object creates and manages a secure network server that allows authorized
* remote users to inspect the (Vanadium-specific) state of the application such as statistics,
* and log files.
*
* <p>When a remote user is invited via the {@link #invite(String, ReadableDuration)} method, a
* principal and blessing is generated that is suitable for use by the
* <a href="https://godoc.org/v.io/x/ref/services/debug/debug">{@code debug browse}</a> command.
*
* By default (if no tags are provided to the constructor), the remote user will not be able
* to invoke any methods that involve reading or writing of application data, instead they
* will be limited to methods that provide debugging metadata only. To enable remote users
* to invoke read and/or write methods, provide
* {@link io.v.v23.security.access.Constants#READ} and/or
* {@link io.v.v23.security.access.Constants#WRITE} to the constructor.
*/
public class RemoteInspectors {
private static final String TAG = "RemoteInspectors";
private static final int BASE64_FLAGS = Base64.URL_SAFE | Base64.NO_WRAP;
private VContext mCtx;
private List<Tag> mTags;
/**
* Creates a secure network server to expose application state to invited remote users.
*
* @param ctx the Vanadium context of the application whose state is to be exposed
* @params tags The set of method tags that determine which methods an invited user can invoke.
* The {@link io.v.v23.security.access.Constants#DEBUG} and
* {@link io.v.v23.security.access.Constants#RESOLVE} tags are always included, whether the
* caller includes it in tags or not.
* @throws VException
*/
public RemoteInspectors(VContext ctx, Tag... tags) throws VException {
// Enhance tags to ensure that it always has DEBUG and RESOLVE.
mTags = new ArrayList<>(tags.length + 2);
boolean hasDebug = false;
boolean hasResolve = false;
for (Tag t : tags) {
if (t.equals(Constants.DEBUG)) {
hasDebug = true;
}
if (t.equals(Constants.RESOLVE)) {
hasResolve = true;
}
mTags.add(t);
}
if (!hasDebug) {
mTags.add(Constants.DEBUG);
}
if (!hasResolve) {
mTags.add(Constants.RESOLVE);
}
mCtx = ctx.withCancel();
Server server = V.getServer(ctx);
if (server == null) {
String mountedName;
try {
mountedName = createMountedName(ctx);
} catch (NoSuchAlgorithmException e) {
throw new VException("Unable to create mounted name:" + e);
}
Log.i(TAG, "Application state should be inspectable via: " + mountedName);
mCtx = V.withNewServer(
V.withListenSpec(mCtx, new ListenSpec("tcp", ":0").withProxy("proxy")),
mountedName,
new InspectedServer() {},
VSecurity.newDefaultAuthorizer());
}
}
/**
* Invite a remote user to inspect state of the application.
*
* @param invitee the name to refer to the remote user as (typically an email address)
* @param duration time after which inspection privileges expire
*
* @return A textual description of how the remote user can access state of the running
* application.
*/
public synchronized String invite(String invitee, ReadableDuration duration)
throws VException {
if (mCtx == null) {
throw new VException("RemoteInspectors.stop already called");
}
Caveat debugOnly = VSecurity.newCaveat(
io.v.v23.security.access.Constants.ACCESS_TAG_CAVEAT, mTags);
Caveat expiration = VSecurity.newExpiryCaveat(DateTime.now().plus(duration));
String privateKey;
VPrincipal delegate;
try {
KeyPairGenerator e = KeyPairGenerator.getInstance("EC");
e.initialize(256);
KeyPair keyPair = e.generateKeyPair();
privateKey = Base64.encodeToString(keyPair.getPrivate().getEncoded(), BASE64_FLAGS);
delegate = VSecurity.newPrincipal(
VSecurity.newSigner(keyPair.getPrivate(), (ECPublicKey) keyPair.getPublic()));
} catch (NoSuchAlgorithmException e) {
throw new VException("Could not mint private key: " + e.getMessage());
}
VPrincipal me = V.getPrincipal(mCtx);
Blessings b = me.bless(
delegate.publicKey(),
me.blessingStore().defaultBlessings(),
"debugger" + io.v.v23.security.Constants.CHAIN_SEPARATOR + invitee,
debugOnly,
expiration);
StringBuilder builder = new StringBuilder()
.append("Please inspect my application using:")
.append('\n')
.append('\n')
.append("debug browse")
.append(" --key=").append(privateKey)
.append(" --blessings=").append(Base64.encodeToString(
VomUtil.encode(b, b.getClass()), BASE64_FLAGS))
.append(" ");
return appendServerAddresses(builder).toString();
}
private StringBuilder appendServerAddresses(StringBuilder sb) {
ServerStatus status = V.getServer(mCtx).getStatus();
for (PublisherEntry e : status.getPublisherStatus()) {
sb = sb.append(e.getName()).append(" ");
}
for (Endpoint ep : status.getEndpoints()) {
sb = sb.append(ep.name()).append(" ");
}
return sb;
}
private static String createMountedName(VContext ctx) throws NoSuchAlgorithmException {
// TODO(ashankar): Use a conventions library here, something like
// https://godoc.org/v.io/v23/conventions
// What follows below is NOT worthy of any form of emulation!
//
// The intent is to create a unique name for this application that it will have
// permission to mount under. The place it has permissions to mount under depends on
// the kind of blessing: <idp>:u:<email>:... should have permissions under user/<email>/...
// while <idp>:o:<appid>:<email> should ultimately go under apps/<appid>/users/<email>/...
// but for now will be placed under tmp/debug/<something>.
//
// The hash of the public key of the principal is used to uniquely identify this particular
// instance of the application.
VPrincipal p = V.getPrincipal(ctx);
String[] myNames = VSecurity.getBlessingNames(p, p.blessingStore().defaultBlessings());
String uid = Base64.encodeToString(
MessageDigest.getInstance("SHA-256").digest(p.publicKey().getEncoded()),
BASE64_FLAGS);
for (String n : myNames) {
// Does this follow the "idp:u:<username>" path?
String[] parts = n.split(io.v.v23.security.Constants.CHAIN_SEPARATOR);
if (parts.length >=3 && parts[1].equals("u")) {
return "users/" + parts[2] + "debug/" + uid;
}
if (parts.length >=4 && parts[1].equals("o")) {
// TODO(ashankar,ribrdb): Replace this with whatever convention we end up with
// when it comes to application blessings.
return "tmp/debug/" + uid;
}
}
return "";
}
}