methods.go raw

   1  // Copyright 2018 Adam S Levy
   2  //
   3  // Permission is hereby granted, free of charge, to any person obtaining a copy
   4  // of this software and associated documentation files (the "Software"), to
   5  // deal in the Software without restriction, including without limitation the
   6  // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
   7  // sell copies of the Software, and to permit persons to whom the Software is
   8  // furnished to do so, subject to the following conditions:
   9  //
  10  // The above copyright notice and this permission notice shall be included in
  11  // all copies or substantial portions of the Software.
  12  //
  13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  18  // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  19  // IN THE SOFTWARE.
  20  
  21  package jsonrpc2
  22  
  23  import (
  24  	"context"
  25  	"encoding/json"
  26  	"errors"
  27  	"fmt"
  28  	"runtime"
  29  )
  30  
  31  // DebugMethodFunc controls whether additional debug information is printed to
  32  // stdout in the event of an InternalError when a MethodFunc is called.
  33  //
  34  // This can be helpful when troubleshooting panics or Internal Errors from a
  35  // MethodFunc.
  36  var DebugMethodFunc = false
  37  
  38  // MethodMap associates method names with MethodFuncs and is passed to
  39  // HTTPRequestHandler to generate a corresponding http.HandlerFunc.
  40  //
  41  // Method names that begin with the word rpc followed by a period character
  42  // (U+002E or ASCII 46) are reserved for rpc-internal methods and extensions
  43  // and MUST NOT be used for anything else. If such a method name is detected
  44  // this will panic. No such internal rpc methods are yet defined in this
  45  // implementation.
  46  type MethodMap map[string]MethodFunc
  47  
  48  // MethodFunc is the function signature used for RPC methods.
  49  //
  50  // MethodFuncs are invoked by the HTTPRequestHandler when a valid Request is
  51  // received. MethodFuncs do not need to concern themselves with the details of
  52  // JSON-RPC 2.0 outside of the "params" field, as all parsing and validation is
  53  // handled by the handler.
  54  //
  55  // The handler will call a MethodFunc with ctx set to the corresponding
  56  // http.Request.Context() and params set to the JSON data from the "params"
  57  // field of the Request. If "params" was omitted or null, params will be nil.
  58  // Otherwise, params is guaranteed to be valid JSON that represents a JSON
  59  // Object or Array.
  60  //
  61  // A MethodFunc is responsible for application specific parsing of params.  A
  62  // MethodFunc should return an ErrorInvalidParams if there is any issue parsing
  63  // expected parameters.
  64  //
  65  // To return a success Response to the client a MethodFunc must return a
  66  // non-error value, that will not cause an error when passed to json.Marshal,
  67  // to be used as the Response.Result. Any marshaling error will cause a panic
  68  // and an Internal Error will be returned to the client.
  69  //
  70  // To return an Error Response to the client, a MethodFunc must return a valid
  71  // Error. A valid Error must use ErrorCodeInvalidParams or any ErrorCode
  72  // outside of the reserved range, and the Error.Data must not cause an error
  73  // when passed to json.Marshal. If the Error is not valid, a panic will occur
  74  // and an Internal Error will be returned to the client.
  75  //
  76  // If a MethodFunc panics or returns any other error, an Internal Error is
  77  // returned to the client. If the returned error is anything other than
  78  // context.Canceled or context.DeadlineExceeded, a panic will occur.
  79  //
  80  // For additional debug output from a MethodFunc regarding the cause of an
  81  // Internal Error, set DebugMethodFunc to true. Information about the method
  82  // call and a stack trace will be printed on panics.
  83  type MethodFunc func(ctx context.Context, params json.RawMessage) interface{}
  84  
  85  // call is used to safely call a method from within an http.HandlerFunc. call
  86  // wraps the actual invocation of the method so that it can recover from panics
  87  // and validate and sanitize the returned Response. If the method panics or
  88  // returns an invalid Response, an Internal Error is returned.
  89  func (method MethodFunc) call(ctx context.Context,
  90  	name string, params json.RawMessage, lgr Logger) (res Response) {
  91  
  92  	var result interface{}
  93  	defer func() {
  94  		if err := recover(); err != nil {
  95  			res.Error = errorInternal(nil)
  96  			if DebugMethodFunc {
  97  				//res.Data = err
  98  				const size = 64 << 10
  99  				buf := make([]byte, size)
 100  				buf = buf[:runtime.Stack(buf, false)]
 101  				lgr.Printf("jsonrpc2: panic running method %q: %v\n",
 102  					name, err)
 103  				lgr.Printf("jsonrpc2: Params: %v\n", string(params))
 104  				lgr.Printf("jsonrpc2: Return: %#v\n", result)
 105  				lgr.Println(string(buf))
 106  			}
 107  		}
 108  	}()
 109  	result = method(ctx, params)
 110  	if err, ok := result.(error); ok {
 111  		var methodErr Error
 112  		if errors.As(err, &methodErr) {
 113  			// InvalidParamsCode is the only reserved ErrorCode
 114  			// MethodFuncs are allowed to return.
 115  			if methodErr.Code == ErrorCodeInvalidParams {
 116  				if methodErr.Message == "" {
 117  					// Ensure the correct message is used if none is supplied.
 118  					methodErr.Message = ErrorMessageInvalidParams
 119  				}
 120  			} else if methodErr.Code.IsReserved() {
 121  				panic(fmt.Errorf("invalid use of %v", methodErr.Code))
 122  			}
 123  			if methodErr.Data != nil {
 124  				// MethodFuncs could return something that
 125  				// cannot be marshaled. Catch that here.
 126  				data, err := json.Marshal(methodErr.Data)
 127  				if err != nil {
 128  					panic(fmt.Errorf("json.Marshal(Error.Data): %w", err))
 129  				}
 130  				methodErr.Data = json.RawMessage(data)
 131  			}
 132  			res.Error = methodErr
 133  			return
 134  		}
 135  
 136  		// MethodFuncs should not normally return a generic error
 137  		// unless they are returning an error that is, or that wraps,
 138  		// context.Canceled or context.DeadlineExceeded.
 139  		//
 140  		// If the http.Request.Context() is canceled then this will
 141  		// never get returned anyway.
 142  		if errors.Is(err, context.Canceled) ||
 143  			errors.Is(err, context.DeadlineExceeded) {
 144  			res.Error = errorInternal(err)
 145  			return
 146  		}
 147  
 148  		// Otherwise, if a MethodFunc intends to return an error to the
 149  		// client it must use the Error type, so this is a program
 150  		// integrity error that should be reported as a panic.
 151  		panic(fmt.Errorf("unexpected error: %w", err))
 152  	}
 153  
 154  	// MethodFuncs could return something that cannot be marshaled. Catch
 155  	// that here.
 156  	data, err := json.Marshal(result)
 157  	if err != nil {
 158  		panic(fmt.Errorf("json.Marshal(result): %w", err))
 159  	}
 160  	res.Result = json.RawMessage(data)
 161  	return
 162  }
 163