github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/lib/dispatch.go (about) 1 package lib 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net/http" 8 "reflect" 9 "strings" 10 "time" 11 12 "github.com/qri-io/qri/auth/token" 13 "github.com/qri-io/qri/event" 14 qhttp "github.com/qri-io/qri/lib/http" 15 "github.com/qri-io/qri/profile" 16 ) 17 18 var ( 19 // ErrDispatchNilInstance indicates that the instance that dispatch as been called on is nil 20 ErrDispatchNilInstance = errors.New("instance is nil, cannot dispatch") 21 // ErrDispatchNilParam indicates that the param passed to dispatch is nil 22 ErrDispatchNilParam = errors.New("param is nil, cannot dispatch") 23 ) 24 25 // dispatcher isolates the dispatch method 26 type dispatcher interface { 27 Dispatch(ctx context.Context, method string, param interface{}) (interface{}, Cursor, error) 28 } 29 30 // MethodSet represents a set of methods to be registered 31 // Each registered method should have 2 input parameters and 1-3 output values 32 // Input: (context.Context, input struct) 33 // Output, 1: (error) 34 // 2: (output, error) 35 // 3: (output, Cursor, error) 36 // The implementation should have the same input and output as the method, except 37 // with the context.Context replaced by a scope. 38 // No other functions are allowed to be defined, other that those that are going to 39 // be registered (as described above) and those that are required by the interface. 40 type MethodSet interface { 41 Name() string 42 Attributes() map[string]AttributeSet 43 } 44 45 // AttributeSet is extra information about each method, such as: http endpoint, 46 // http verb, (TODO) permissions, and (TODO) other metadata 47 // Each method is required to have associated attributes in order to successfully register 48 // Variables are exported so that external packages such as docs can access them 49 type AttributeSet struct { 50 Endpoint qhttp.APIEndpoint 51 HTTPVerb string 52 // the default source used for resolving references 53 DefaultSource string 54 // whether to deny RPC for this endpoint, normal HTTP may still be allowed 55 DenyRPC bool 56 } 57 58 // Dispatch is a system for handling calls to lib. Should only be called by top-level lib methods. 59 // 60 // When programs are using qri as a library (such as the `cmd` package), calls to `lib` will 61 // arrive at dispatch, before being routed to the actual implementation routine. This solves 62 // a few problems: 63 // 1) Multiple methods can be running on qri at once, dispatch will schedule as needed (TODO) 64 // 2) Access to core qri data structures (like logbook) can be handled safetly (TODO) 65 // 3) User identity, permissions, etc is scoped to a single call, not the global process (TODO) 66 // 4) The qri http api maps directly onto dispatch's behavior, leading to a simpler api 67 // 5) A `qri connect` process can be transparently forwarded a method call with little work 68 // 69 // At construction time, the Instance registers all methods that dispatch can access, as well 70 // as the input and output parameters for those methods, and associates a string name for each 71 // method. Dispatch works by looking up that method name, constructing the necessary input, 72 // then invoking the actual implementation. Dispatch returns the custom value from the 73 // implementation, then a non-nil Cursor if the method supports pagination, then an error or nil. 74 func (inst *Instance) Dispatch(ctx context.Context, method string, param interface{}) (res interface{}, cur Cursor, err error) { 75 source := "" 76 return inst.dispatchMethodCall(ctx, method, param, source) 77 } 78 79 // Dispatch calls the same instance Dispatch but with an explicit source for ref resolution 80 func (isw *InstanceSourceWrap) Dispatch(ctx context.Context, method string, param interface{}) (res interface{}, cur Cursor, err error) { 81 return isw.inst.dispatchMethodCall(ctx, method, param, isw.source) 82 } 83 84 func (inst *Instance) dispatchMethodCall(ctx context.Context, method string, param interface{}, source string) (res interface{}, cur Cursor, err error) { 85 if inst == nil { 86 return nil, nil, ErrDispatchNilInstance 87 } 88 if param == nil || (reflect.ValueOf(param).Kind() == reflect.Ptr && reflect.ValueOf(param).IsNil()) { 89 return nil, nil, ErrDispatchNilParam 90 } 91 92 // If the input parameters has a Validate method, call it 93 if validator, ok := param.(ParamValidator); ok { 94 err = validator.Validate() 95 if err != nil { 96 return nil, nil, err 97 } 98 } 99 100 // If the http rpc layer is engaged, use it to dispatch methods 101 // This happens when another process is running `qri connect` 102 if inst.http != nil { 103 if tok := token.FromCtx(ctx); tok == "" { 104 // If no token exists, create one from configured profile private key & 105 // add it to the request context 106 // TODO(b5): we're falling back to the configured user to make requests, 107 // is this the right default? 108 p, err := profile.NewProfile(inst.cfg.Profile) 109 if err != nil { 110 return nil, nil, err 111 } 112 tokstr, err := token.NewPrivKeyAuthToken(p.PrivKey, p.ID.Encode(), time.Minute) 113 if err != nil { 114 return nil, nil, err 115 } 116 ctx = token.AddToContext(ctx, tokstr) 117 } 118 119 if c, ok := inst.regMethods.lookup(method); ok { 120 if c.DenyRPC { 121 return nil, nil, qhttp.ErrUnsupportedRPC 122 } 123 if c.OutType != nil { 124 out := reflect.New(c.OutType) 125 res = out.Interface() 126 } 127 // TODO(ramfox): dispatch is still unable to give enough details to the url 128 // (because it doesn't know how or what param information to put into the url or query) 129 // for it to reliably use GET. All POSTs w/ content type application json work, however. 130 // we may want to just flat out say that as an RPC layer, dispatch will only ever use 131 // json POST to communicate. 132 err = inst.http.CallMethod(ctx, c.Endpoint, http.MethodPost, source, param, res) 133 if err != nil { 134 return nil, nil, err 135 } 136 cur = nil 137 var inf interface{} 138 if res != nil { 139 out := reflect.ValueOf(res) 140 out = out.Elem() 141 inf = out.Interface() 142 } 143 return inf, cur, nil 144 } 145 return nil, nil, fmt.Errorf("method %q not found", method) 146 } 147 148 // Look up the method for the given signifier 149 if c, ok := inst.regMethods.lookup(method); ok { 150 // If this method has a default source and no override exists, use that 151 // default instead 152 if source == "" { 153 source = c.Source 154 } 155 // Construct the isolated scope for this call 156 // TODO(dustmop): Add user authentication, profile, identity, etc 157 // TODO(dustmop): Also determine if the method is read-only vs read-write, 158 // and only execute a single read-write method at a time 159 // Eventually, the data that lives in scope should be immutable for its lifetime, 160 // or use copy-on-write semantics, so that one method running at the same time as 161 // another cannot modify the out-of-scope data of the other. This will mostly 162 // involve making copies of the right things 163 scope, err := newScope(ctx, inst, method, source) 164 if err != nil { 165 return nil, nil, err 166 } 167 168 // Handle filepaths in the params by calling qfs.Abs on each of them 169 param = normalizeInputParams(param) 170 171 inst.Bus().Publish(ctx, event.ETDispatchMethodCall, event.DispatchCall{ 172 Method: method, 173 Params: param, 174 }) 175 176 // Construct the parameter list for the function call, then call it 177 args := make([]reflect.Value, 3) 178 args[0] = reflect.ValueOf(c.Impl) 179 args[1] = reflect.ValueOf(scope) 180 args[2] = reflect.ValueOf(param) 181 outVals := c.Func.Call(args) 182 183 // TODO(dustmop): If the method wrote to our internal data structures, like 184 // refstore, logbook, etc, serialize and commit those changes here 185 186 // Validate the return values. 187 if len(outVals) < 1 || len(outVals) > 3 { 188 return nil, nil, fmt.Errorf("wrong number of return values: %d", len(outVals)) 189 } 190 // Extract the concrete typed values from the method return 191 var out interface{} 192 var cur Cursor 193 // There are either 1, 2, or 3 output values: 194 // 1: func() (err) 195 // 2: func() (out, err) 196 // 3: func() (out, cur, err) 197 if len(outVals) == 2 || len(outVals) == 3 { 198 out = outVals[0].Interface() 199 } 200 if len(outVals) == 3 { 201 curVal := outVals[1].Interface() 202 if c, ok := curVal.(Cursor); ok { 203 cur = c 204 } 205 } 206 // Error always comes last 207 errVal := outVals[len(outVals)-1].Interface() 208 if errVal == nil { 209 return out, cur, nil 210 } 211 if err, ok := errVal.(error); ok { 212 return out, cur, err 213 } 214 return nil, nil, fmt.Errorf("last return value should be an error, got: %v", errVal) 215 } 216 return nil, nil, fmt.Errorf("method %q not found", method) 217 } 218 219 // ParamValidator may be implemented by method parameter structs, and if so 220 // then Dispatch will validate the parameters are okay before calling anything 221 type ParamValidator interface { 222 Validate() error 223 } 224 225 // NewInputParam takes a method name that has been registered, and constructs 226 // an instance of that input parameter 227 func (inst *Instance) NewInputParam(method string) interface{} { 228 if c, ok := inst.regMethods.lookup(method); ok { 229 obj := reflect.New(c.InType) 230 return obj.Interface() 231 } 232 return nil 233 } 234 235 // regMethodSet represents a set of registered methods 236 type regMethodSet struct { 237 reg map[string]callable 238 } 239 240 // lookup finds the callable structure with the given method name 241 func (r *regMethodSet) lookup(method string) (*callable, bool) { 242 if c, ok := r.reg[method]; ok { 243 return &c, true 244 } 245 return nil, false 246 } 247 248 type callable struct { 249 Impl interface{} 250 Func reflect.Value 251 InType reflect.Type 252 OutType reflect.Type 253 RetCursor bool 254 Endpoint qhttp.APIEndpoint 255 Verb string 256 Source string 257 DenyRPC bool 258 } 259 260 // AllMethods returns a method set for documentation purposes 261 // TODO(arqu): this is intended to merge with RegisterMethods as it's only exposed 262 // for generating the OpenAPI spec 263 func (inst *Instance) AllMethods() []MethodSet { 264 return []MethodSet{ 265 inst.Access(), 266 inst.Collection(), 267 inst.Config(), 268 inst.Dataset(), 269 inst.Diff(), 270 inst.Log(), 271 inst.Peer(), 272 inst.Profile(), 273 inst.Registry(), 274 inst.Follow(), 275 inst.Remote(), 276 inst.Search(), 277 inst.Automation(), 278 } 279 } 280 281 // RegisterMethods iterates the methods provided by the lib API, and makes them visible to dispatch 282 func (inst *Instance) RegisterMethods() { 283 reg := make(map[string]callable) 284 inst.registerOne("access", inst.Access(), accessImpl{}, reg) 285 inst.registerOne("automation", inst.Automation(), automationImpl{}, reg) 286 inst.registerOne("collection", inst.Collection(), collectionImpl{}, reg) 287 inst.registerOne("config", inst.Config(), configImpl{}, reg) 288 inst.registerOne("dataset", inst.Dataset(), datasetImpl{}, reg) 289 inst.registerOne("diff", inst.Diff(), diffImpl{}, reg) 290 inst.registerOne("log", inst.Log(), logImpl{}, reg) 291 inst.registerOne("peer", inst.Peer(), peerImpl{}, reg) 292 inst.registerOne("profile", inst.Profile(), profileImpl{}, reg) 293 inst.registerOne("registry", inst.Registry(), registryImpl{}, reg) 294 inst.registerOne("follow", inst.Follow(), followImpl{}, reg) 295 inst.registerOne("remote", inst.Remote(), remoteImpl{}, reg) 296 inst.registerOne("search", inst.Search(), searchImpl{}, reg) 297 inst.regMethods = ®MethodSet{reg: reg} 298 } 299 300 func (inst *Instance) registerOne(ourName string, methods MethodSet, impl interface{}, reg map[string]callable) { 301 implType := reflect.TypeOf(impl) 302 msetType := reflect.TypeOf(methods) 303 methodMap := inst.buildMethodMap(methods) 304 // Validate that the methodSet has the correct name 305 if methods.Name() != ourName { 306 regFail("registration wrong name, expect: %q, got: %q", ourName, methods.Name()) 307 } 308 // Iterate methods on the implementation, register those that have the right signature 309 num := implType.NumMethod() 310 for k := 0; k < num; k++ { 311 i := implType.Method(k) 312 lowerName := strings.ToLower(i.Name) 313 funcName := fmt.Sprintf("%s.%s", ourName, lowerName) 314 315 // Validate the parameters to the implementation 316 // should have 3 input parameters: (receiver, scope, input struct) 317 // should have 1-3 output parametres: ([output value]?, [cursor]?, error) 318 f := i.Type 319 if f.NumIn() != 3 { 320 regFail("%s: bad number of inputs: %d", funcName, f.NumIn()) 321 } 322 // First input must be the receiver 323 inType := f.In(0) 324 if inType != implType { 325 regFail("%s: first input param should be impl, got %v", funcName, inType) 326 } 327 // Second input must be a scope 328 inType = f.In(1) 329 if inType.Name() != "scope" { 330 regFail("%s: second input param should be scope, got %v", funcName, inType) 331 } 332 // Third input is a pointer to the input struct 333 inType = f.In(2) 334 if inType.Kind() != reflect.Ptr { 335 regFail("%s: third input param must be a struct pointer, got %v", funcName, inType) 336 } 337 inType = inType.Elem() 338 if inType.Kind() != reflect.Struct { 339 regFail("%s: third input param must be a struct pointer, got %v", funcName, inType) 340 } 341 // Validate the output values of the implementation 342 numOuts := f.NumOut() 343 if numOuts < 1 || numOuts > 3 { 344 regFail("%s: bad number of outputs: %d", funcName, numOuts) 345 } 346 // Validate output values 347 var outType reflect.Type 348 returnsCursor := false 349 if numOuts == 2 || numOuts == 3 { 350 // First output is anything 351 outType = f.Out(0) 352 } 353 if numOuts == 3 { 354 // Second output must be a cursor 355 outCursorType := f.Out(1) 356 if !strings.HasSuffix(outCursorType.Name(), "Cursor") { 357 regFail("%s: second output val must be a cursor, got %v", funcName, outCursorType) 358 } 359 returnsCursor = true 360 } 361 // Last output must be an error 362 outErrType := f.Out(numOuts - 1) 363 if outErrType.Name() != "error" { 364 regFail("%s: last output val should be error, got %v", funcName, outErrType) 365 } 366 367 // Validate the parameters to the method that matches the implementation 368 // should have 3 input parameters: (receiver, context.Context, input struct: same as impl]) 369 // should have 1-3 output parametres: ([output value: same as impl], [cursor], error) 370 m, ok := methodMap[i.Name] 371 if !ok { 372 regFail("method %s not found on MethodSet", i.Name) 373 } 374 f = m.Type 375 if f.NumIn() != 3 { 376 regFail("%s: bad number of inputs: %d", funcName, f.NumIn()) 377 } 378 // First input must be the receiver 379 mType := f.In(0) 380 if mType.Name() != msetType.Name() { 381 regFail("%s: first input param should be impl, got %v", funcName, mType) 382 } 383 // Second input must be a context 384 mType = f.In(1) 385 if mType.Name() != "Context" { 386 regFail("%s: second input param should be context.Context, got %v", funcName, mType) 387 } 388 // Third input is a pointer to the input struct 389 mType = f.In(2) 390 if mType.Kind() != reflect.Ptr { 391 regFail("%s: third input param must be a pointer, got %v", funcName, mType) 392 } 393 mType = mType.Elem() 394 if mType != inType { 395 regFail("%s: third input param must match impl, expect %v, got %v", funcName, inType, mType) 396 } 397 // Validate the output values of the implementation 398 msetNumOuts := f.NumOut() 399 if msetNumOuts < 1 || msetNumOuts > 3 { 400 regFail("%s: bad number of outputs: %d", funcName, f.NumOut()) 401 } 402 // First output, if there's more than 1, matches the impl output 403 if msetNumOuts == 2 || msetNumOuts == 3 { 404 mType = f.Out(0) 405 if mType != outType { 406 regFail("%s: first output val must match impl, expect %v, got %v", funcName, outType, mType) 407 } 408 } 409 // Second output, if there are three, must be a cursor 410 if msetNumOuts == 3 { 411 mType = f.Out(1) 412 if mType.Name() != "Cursor" { 413 regFail("%s: second output val must match a cursor, got %v", funcName, mType) 414 } 415 } 416 // Last output must be an error 417 mType = f.Out(msetNumOuts - 1) 418 if mType.Name() != "error" { 419 regFail("%s: last output val should be error, got %v", funcName, mType) 420 } 421 422 // Remove this method from the methodSetMap now that it has been processed 423 delete(methodMap, i.Name) 424 425 // Additional attributes for the method are found in the Attributes 426 amap := methods.Attributes() 427 methodAttrs, ok := amap[lowerName] 428 if !ok { 429 regFail("not in Attributes: %s.%s", ourName, lowerName) 430 } 431 validateMethodAttrs(lowerName, methodAttrs) 432 433 // Save the method to the registration table 434 reg[funcName] = callable{ 435 Impl: impl, 436 Func: i.Func, 437 InType: inType, 438 OutType: outType, 439 RetCursor: returnsCursor, 440 Endpoint: methodAttrs.Endpoint, 441 Verb: methodAttrs.HTTPVerb, 442 Source: methodAttrs.DefaultSource, 443 DenyRPC: methodAttrs.DenyRPC, 444 } 445 } 446 447 for k := range methodMap { 448 if k != "Name" && k != "Attributes" { 449 regFail("%s: did not find implementation for method %s", msetType, k) 450 } 451 } 452 } 453 454 func regFail(fstr string, vals ...interface{}) { 455 panic(fmt.Sprintf(fstr, vals...)) 456 } 457 458 func validateMethodAttrs(methodName string, attrs AttributeSet) { 459 // If endpoint and verb are not set, then RPC is denied, nothing to validate 460 // TODO(dustmop): Technically this is denying all HTTP, not just RPC. Consider 461 // separating HTTP and RPC denial 462 if attrs.Endpoint == "" && attrs.HTTPVerb == "" { 463 return 464 } 465 if !strings.HasPrefix(string(attrs.Endpoint), "/") { 466 regFail("%s: endpoint URL must start with /, got %q", methodName, attrs.Endpoint) 467 } 468 if !stringOneOf(attrs.HTTPVerb, []string{http.MethodGet, http.MethodPost, http.MethodPut}) { 469 regFail("%s: unknown http verb, got %q", methodName, attrs.HTTPVerb) 470 } 471 } 472 473 func stringOneOf(needle string, haystack []string) bool { 474 for _, each := range haystack { 475 if needle == each { 476 return true 477 } 478 } 479 return false 480 } 481 482 func (inst *Instance) buildMethodMap(impl interface{}) map[string]reflect.Method { 483 result := make(map[string]reflect.Method) 484 implType := reflect.TypeOf(impl) 485 num := implType.NumMethod() 486 for k := 0; k < num; k++ { 487 m := implType.Method(k) 488 result[m.Name] = m 489 } 490 return result 491 } 492 493 func dispatchMethodName(m MethodSet, funcName string) string { 494 lowerName := strings.ToLower(funcName) 495 return fmt.Sprintf("%s.%s", m.Name(), lowerName) 496 } 497 498 func dispatchReturnError(got interface{}, err error) error { 499 if got != nil { 500 log.Errorf("type mismatch: %v of type %s", got, reflect.TypeOf(got)) 501 } 502 return err 503 }