blob: 564c1ebf11e5422562acf8cd96345a6b29fa1282 [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 main
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/base64"
"fmt"
"strings"
"time"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/security"
"v.io/v23/security/access"
"v.io/v23/vom"
"v.io/x/lib/cmdline"
seclib "v.io/x/ref/lib/security"
"v.io/x/ref/lib/signals"
"v.io/x/ref/lib/v23cmd"
"v.io/x/ref/services/debug/debug/browseserver"
)
func init() {
cmdBrowse.Flags.StringVar(&flagBrowseAddr, "addr", "", "Address on which the interactive HTTP server will listen. For example, localhost:14141. If empty, defaults to localhost:<some random port>")
cmdBrowse.Flags.BoolVar(&flagBrowseLog, "log", true, "If true, log debug data obtained so that if a subsequent refresh from the browser fails, previously obtained information is available from the log file")
cmdBrowse.Flags.StringVar(&flagBrowseBlessings, "blessings", "", "If non-empty, points to the blessings required to debug the process. This is typically obtained via 'debug delegate' run by the owner of the remote process")
cmdBrowse.Flags.StringVar(&flagBrowsePrivateKey, "key", "", "If non-empty, must be accompanied with --blessings with a value obtained via 'debug delegate' run by the owner of the remote process")
cmdBrowse.Flags.StringVar(&flagBrowseAssets, "assets", "", "If non-empty, load assets from this directory.")
cmdDelegate.Flags.StringVar(&flagDelegateAccess, "access", "resolve,debug", "Comma-separated list of access tags on methods that can be invoked by the delegate")
}
var (
flagBrowseAddr string
flagBrowseLog bool
flagBrowseBlessings string
flagBrowsePrivateKey string
flagBrowseAssets string
flagDelegateAccess string
cmdBrowse = &cmdline.Command{
Runner: v23cmd.RunnerFunc(runBrowse),
Name: "browse",
Short: "Starts an interactive interface for debugging",
Long: `
Starts a webserver with a URL that when visited allows for inspection of a
remote process via a web browser.
This differs from browser.v.io in a few important ways:
(a) Does not require a chrome extension,
(b) Is not tied into the v.io cloud services
(c) Can be setup with alternative different credentials,
(d) The interface is more geared towards debugging a server than general purpose namespace browsing.
While (d) is easily overcome by sharing code between the two, (a), (b) & (c)
are not easy to work around. Of course, the down-side here is that this
requires explicit command-line invocation instead of being just a URL anyone
can visit (https://browser.v.io).
A dump of some possible future features:
TODO(ashankar):?
(1) Trace browsing: Browse traces at the remote server, and possible force
the collection of some traces (avoiding the need to restart the remote server
with flags like --v23.vtrace.collect-regexp for example). In the mean time,
use the 'vtrace' command (instead of the 'browse' command) for this purpose.
(2) Log offsets: Log files can be large and currently the logging endpoint
of this interface downloads the full log file from the beginning. The ability
to start looking at the logs only from a specified offset might be useful
for these large files.
(3) Signature: Display the interfaces, types etc. defined by any suffix in the
remote process. in the mean time, use the 'vrpc signature' command for this purpose.
`,
ArgsName: "<name> [<name>] [<name>]",
ArgsLong: `
<name> is the vanadium object name of the remote process to inspect.
If multiple names are provided, they are considered equivalent and any one of them that can
is accessible is used.`,
}
cmdDelegate = &cmdline.Command{
Runner: v23cmd.RunnerFunc(runDelegate),
Name: "delegate",
Short: "Create credentials to delegate debugging to another user",
Long: `
Generates credentials (private key and blessings) required to debug remote
processes owned by the caller and prints out the command that the delegate can
run. The delegation limits the bearer of the token to invoke methods on the
remote process only if they have the "access.Debug" tag on them, and this token
is valid only for limited amount of time. For example, if Alice wants Bob to
debug a remote process owned by her for the next 2 hours, she runs:
debug delegate my-friend-bob 2h myservices/myservice
And sends Bob the output of this command. Bob will then be able to inspect the
remote process as myservices/myservice with the same authorization as Alice.
`,
ArgsName: "<to> <duration> [<name>]",
ArgsLong: `
<to> is an identifier to provide to the delegate.
<duration> is the time period to delegate for (e.g., 1h for 1 hour)
<name> (optional) is the vanadium object name of the remote process to inspect
`,
}
)
func runBrowse(ctx *context.T, env *cmdline.Env, args []string) error {
if len(args) == 0 {
return env.UsageErrorf("must provide at least a single vanadium object name")
}
// For now, require that either both --key and --blessings be set, or
// neither be. This ensures that no persistent changes are made to the principal
// embedded in 'ctx' at this point.
if (len(flagBrowsePrivateKey) != 0) != (len(flagBrowseBlessings) != 0) {
return env.UsageErrorf("either both --key and --blessings should be set, or neither should be")
}
if len(flagBrowsePrivateKey) != 0 {
keybytes, err := base64.URLEncoding.DecodeString(flagBrowsePrivateKey)
if err != nil {
return fmt.Errorf("bad value for --key: %v", err)
}
key, err := x509.ParseECPrivateKey(keybytes)
if err != nil {
// Try with PKCS#8
tmp, err := x509.ParsePKCS8PrivateKey(keybytes)
if err != nil {
return fmt.Errorf("bad value for --key: %v", err)
}
var ok bool
if key, ok = tmp.(*ecdsa.PrivateKey); !ok {
return fmt.Errorf("expected an ECDSA private in in --key, got %T", tmp)
}
}
principal, err := seclib.NewPrincipalFromSigner(security.NewInMemoryECDSASigner(key), nil)
if err != nil {
return fmt.Errorf("unable to use --key: %v", err)
}
if ctx, err = v23.WithPrincipal(ctx, principal); err != nil {
return err
}
}
if len(flagBrowseBlessings) != 0 {
blessingbytes, err := base64.URLEncoding.DecodeString(flagBrowseBlessings)
if err != nil {
return fmt.Errorf("bad value for --blessings: %v", err)
}
var blessings security.Blessings
if err := vom.Decode(blessingbytes, &blessings); err != nil {
return fmt.Errorf("bad value for --blessings: %v", err)
}
if _, err := v23.GetPrincipal(ctx).BlessingStore().Set(blessings, security.AllPrincipals); err != nil {
if len(flagBrowsePrivateKey) == 0 {
return fmt.Errorf("cannot use --blessings, mismatch with --key? (%v)", err)
}
return fmt.Errorf("cannot use --blessings: %v", err)
}
if err := security.AddToRoots(v23.GetPrincipal(ctx), blessings); err != nil {
return fmt.Errorf("failed to add --blessings to the set of recognized roots: %v", err)
}
}
name, err := selectName(ctx, args)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(ctx)
go func() {
<-signals.ShutdownOnSignals(ctx)
cancel()
}()
return browseserver.Serve(ctx, flagBrowseAddr, name, timeout, flagBrowseLog, flagBrowseAssets)
}
func selectName(ctx *context.T, options []string) (string, error) {
// TODO(ashankar,mattr): This is way more complicated than it should
// be. What we really want is to be able to have the RPC system take
// in these many names and pick the best one (accessible, shorted path)
// in every subsequent call. For now, just try them all in parallel and
// return the first one that works.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
working := make(chan int, len(options))
errorch := make(chan error, len(options))
for i := range options {
go func(idx int) {
pc, err := v23.GetClient(ctx).PinConnection(ctx, options[idx])
if err != nil {
errorch <- err
return
}
pc.Unpin()
working <- idx
}(i)
}
var errors []error
for i := 0; i < len(options); i++ {
select {
case idx := <-working:
return options[idx], nil
case err := <-errorch:
errors = append(errors, err)
}
}
return "", fmt.Errorf("failed to contact server: %v", errors)
}
func delegateAccessCaveat() (security.Caveat, error) {
strs := strings.Split(flagDelegateAccess, ",")
var tags []access.Tag
for _, s := range strs {
ls := strings.ToLower(s)
found := false
for _, t := range access.AllTypicalTags() {
lt := strings.ToLower(string(t))
if ls == lt {
tags = append(tags, t)
found = true
break
}
}
if !found {
return security.Caveat{}, fmt.Errorf("invalid --access: [%s] is not in %v", s, access.AllTypicalTags())
}
}
if len(tags) == 0 {
var all []string
for _, t := range access.AllTypicalTags() {
all = append(all, string(t))
}
return security.Caveat{}, fmt.Errorf("must specify a non-empty --access. To grant all access, use --access=%s", strings.Join(all, ","))
}
return access.NewAccessTagCaveat(tags...)
}
func runDelegate(ctx *context.T, env *cmdline.Env, args []string) error {
if len(args) < 2 || len(args) > 3 {
return env.UsageErrorf("got %d arguments, expecting 2 or 3", len(args))
}
extension := args[0]
duration, err := time.ParseDuration(args[1])
if err != nil {
return env.UsageErrorf("failed to parse <duration>: %v", err)
}
name := "<name>"
if len(args) == 3 {
name = args[2]
}
// Create a new private key.
pub, priv, err := seclib.NewPrincipalKey()
if err != nil {
return err
}
privbytes, err := x509.MarshalECPrivateKey(priv)
if err != nil {
return err
}
// Create a blessings
var caveats []security.Caveat
if c, err := delegateAccessCaveat(); err != nil {
return err
} else {
caveats = append(caveats, c)
}
if c, err := security.NewExpiryCaveat(time.Now().Add(duration)); err != nil {
return err
} else {
caveats = append(caveats, c)
}
p := v23.GetPrincipal(ctx)
b, _ := p.BlessingStore().Default()
blessing, err := p.Bless(pub, b, extension, caveats[0], caveats[1:]...)
if err != nil {
return err
}
blessingbytes, err := vom.Encode(blessing)
if err != nil {
return err
}
fmt.Printf(`Your delegate will be seen as %v at your server. Ask them to run:
debug browse \
--key=%q \
--blessings=%q \
%q
`,
blessing,
base64.URLEncoding.EncodeToString(privbytes),
base64.URLEncoding.EncodeToString(blessingbytes),
name)
return nil
}