/
browse.go
296 lines (275 loc) · 10.2 KB
/
browse.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
// 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
}