| // 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 |
| } |