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