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 = &regMethodSet{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  }