mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-27 02:40:30 +00:00
680 lines
17 KiB
Go
680 lines
17 KiB
Go
// Package jsonrpc2 provides a client and server implementation of
|
|
// [JSON-RPC 2.0](http://www.jsonrpc.org/specification).
|
|
package jsonrpc2
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"strconv"
|
|
"sync"
|
|
)
|
|
|
|
// JSONRPC2 describes an interface for issuing requests that speak the
|
|
// JSON-RPC 2 protocol. It isn't really necessary for this package
|
|
// itself, but is useful for external users that use the interface as
|
|
// an API boundary.
|
|
type JSONRPC2 interface {
|
|
// Call issues a standard request (http://www.jsonrpc.org/specification#request_object).
|
|
Call(ctx context.Context, method string, params, result interface{}, opt ...CallOption) error
|
|
|
|
// Notify issues a notification request (http://www.jsonrpc.org/specification#notification).
|
|
Notify(ctx context.Context, method string, params interface{}, opt ...CallOption) error
|
|
|
|
// Close closes the underlying connection, if it exists.
|
|
Close() error
|
|
}
|
|
|
|
// Request represents a JSON-RPC request or
|
|
// notification. See
|
|
// http://www.jsonrpc.org/specification#request_object and
|
|
// http://www.jsonrpc.org/specification#notification.
|
|
type Request struct {
|
|
Method string `json:"method"`
|
|
Params *json.RawMessage `json:"params,omitempty"`
|
|
ID ID `json:"id"`
|
|
Notif bool `json:"-"`
|
|
|
|
// Meta optionally provides metadata to include in the request.
|
|
//
|
|
// NOTE: It is not part of spec. However, it is useful for propogating
|
|
// tracing context, etc.
|
|
Meta *json.RawMessage `json:"meta,omitempty"`
|
|
}
|
|
|
|
// MarshalJSON implements json.Marshaler and adds the "jsonrpc":"2.0"
|
|
// property.
|
|
func (r Request) MarshalJSON() ([]byte, error) {
|
|
r2 := struct {
|
|
Method string `json:"method"`
|
|
Params *json.RawMessage `json:"params,omitempty"`
|
|
ID *ID `json:"id,omitempty"`
|
|
Meta *json.RawMessage `json:"meta,omitempty"`
|
|
JSONRPC string `json:"jsonrpc"`
|
|
}{
|
|
Method: r.Method,
|
|
Params: r.Params,
|
|
Meta: r.Meta,
|
|
JSONRPC: "2.0",
|
|
}
|
|
if !r.Notif {
|
|
r2.ID = &r.ID
|
|
}
|
|
return json.Marshal(r2)
|
|
}
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler.
|
|
func (r *Request) UnmarshalJSON(data []byte) error {
|
|
var r2 struct {
|
|
Method string `json:"method"`
|
|
Params *json.RawMessage `json:"params,omitempty"`
|
|
Meta *json.RawMessage `json:"meta,omitempty"`
|
|
ID *ID `json:"id"`
|
|
}
|
|
|
|
// Detect if the "params" field is JSON "null" or just not present
|
|
// by seeing if the field gets overwritten to nil.
|
|
r2.Params = &json.RawMessage{}
|
|
|
|
if err := json.Unmarshal(data, &r2); err != nil {
|
|
return err
|
|
}
|
|
r.Method = r2.Method
|
|
if r2.Params == nil {
|
|
r.Params = &jsonNull
|
|
} else if len(*r2.Params) == 0 {
|
|
r.Params = nil
|
|
} else {
|
|
r.Params = r2.Params
|
|
}
|
|
r.Meta = r2.Meta
|
|
if r2.ID == nil {
|
|
r.ID = ID{}
|
|
r.Notif = true
|
|
} else {
|
|
r.ID = *r2.ID
|
|
r.Notif = false
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetParams sets r.Params to the JSON representation of v. If JSON
|
|
// marshaling fails, it returns an error.
|
|
func (r *Request) SetParams(v interface{}) error {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Params = (*json.RawMessage)(&b)
|
|
return nil
|
|
}
|
|
|
|
// SetMeta sets r.Meta to the JSON representation of v. If JSON
|
|
// marshaling fails, it returns an error.
|
|
func (r *Request) SetMeta(v interface{}) error {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Meta = (*json.RawMessage)(&b)
|
|
return nil
|
|
}
|
|
|
|
// Response represents a JSON-RPC response. See
|
|
// http://www.jsonrpc.org/specification#response_object.
|
|
type Response struct {
|
|
ID ID `json:"id"`
|
|
Result *json.RawMessage `json:"result"`
|
|
Error *Error `json:"error,omitempty"`
|
|
|
|
// Meta optionally provides metadata to include in the response.
|
|
//
|
|
// NOTE: It is not part of spec. However, it is useful for propogating
|
|
// tracing context, etc.
|
|
Meta *json.RawMessage `json:"meta,omitempty"`
|
|
|
|
// SPEC NOTE: The spec says "If there was an error in detecting
|
|
// the id in the Request object (e.g. Parse error/Invalid
|
|
// Request), it MUST be Null." If we made the ID field nullable,
|
|
// then we'd have to make it a pointer type. For simplicity, we're
|
|
// ignoring the case where there was an error in detecting the ID
|
|
// in the Request object.
|
|
}
|
|
|
|
// MarshalJSON implements json.Marshaler and adds the "jsonrpc":"2.0"
|
|
// property.
|
|
func (r Response) MarshalJSON() ([]byte, error) {
|
|
if (r.Result == nil || len(*r.Result) == 0) && r.Error == nil {
|
|
return nil, errors.New("can't marshal *jsonrpc2.Response (must have result or error)")
|
|
}
|
|
type tmpType Response // avoid infinite MarshalJSON recursion
|
|
b, err := json.Marshal(tmpType(r))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b = append(b[:len(b)-1], []byte(`,"jsonrpc":"2.0"}`)...)
|
|
return b, nil
|
|
}
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler.
|
|
func (r *Response) UnmarshalJSON(data []byte) error {
|
|
type tmpType Response
|
|
|
|
// Detect if the "result" field is JSON "null" or just not present
|
|
// by seeing if the field gets overwritten to nil.
|
|
*r = Response{Result: &json.RawMessage{}}
|
|
|
|
if err := json.Unmarshal(data, (*tmpType)(r)); err != nil {
|
|
return err
|
|
}
|
|
if r.Result == nil { // JSON "null"
|
|
r.Result = &jsonNull
|
|
} else if len(*r.Result) == 0 {
|
|
r.Result = nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetResult sets r.Result to the JSON representation of v. If JSON
|
|
// marshaling fails, it returns an error.
|
|
func (r *Response) SetResult(v interface{}) error {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Result = (*json.RawMessage)(&b)
|
|
return nil
|
|
}
|
|
|
|
// Error represents a JSON-RPC response error.
|
|
type Error struct {
|
|
Code int64 `json:"code"`
|
|
Message string `json:"message"`
|
|
Data *json.RawMessage `json:"data"`
|
|
}
|
|
|
|
// SetError sets e.Error to the JSON representation of v. If JSON
|
|
// marshaling fails, it panics.
|
|
func (e *Error) SetError(v interface{}) {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
panic("Error.SetData: " + err.Error())
|
|
}
|
|
e.Data = (*json.RawMessage)(&b)
|
|
}
|
|
|
|
// Error implements the Go error interface.
|
|
func (e *Error) Error() string {
|
|
return fmt.Sprintf("jsonrpc2: code %v message: %s", e.Code, e.Message)
|
|
}
|
|
|
|
const (
|
|
// Errors defined in the JSON-RPC spec. See
|
|
// http://www.jsonrpc.org/specification#error_object.
|
|
CodeParseError = -32700
|
|
CodeInvalidRequest = -32600
|
|
CodeMethodNotFound = -32601
|
|
CodeInvalidParams = -32602
|
|
CodeInternalError = -32603
|
|
codeServerErrorStart = -32099
|
|
codeServerErrorEnd = -32000
|
|
)
|
|
|
|
// Handler handles JSON-RPC requests and notifications.
|
|
type Handler interface {
|
|
// Handle is called to handle a request.
|
|
Handle(context.Context, *Conn, *Request)
|
|
}
|
|
|
|
// ID represents a JSON-RPC 2.0 request ID, which may be either a
|
|
// string or number (or null, which is unsupported).
|
|
type ID struct {
|
|
// At most one of Num or Str may be nonzero. If both are zero
|
|
// valued, then IsNum specifies which field's value is to be used
|
|
// as the ID.
|
|
Num uint64
|
|
Str string
|
|
|
|
// IsString controls whether the Num or Str field's value should be
|
|
// used as the ID, when both are zero valued. It must always be
|
|
// set to true if the request ID is a string.
|
|
IsString bool
|
|
}
|
|
|
|
func (id ID) String() string {
|
|
if id.IsString {
|
|
return strconv.Quote(id.Str)
|
|
}
|
|
return strconv.FormatUint(id.Num, 10)
|
|
}
|
|
|
|
// MarshalJSON implements json.Marshaler.
|
|
func (id ID) MarshalJSON() ([]byte, error) {
|
|
if id.IsString {
|
|
return json.Marshal(id.Str)
|
|
}
|
|
return json.Marshal(id.Num)
|
|
}
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler.
|
|
func (id *ID) UnmarshalJSON(data []byte) error {
|
|
// Support both uint64 and string IDs.
|
|
var v uint64
|
|
if err := json.Unmarshal(data, &v); err == nil {
|
|
*id = ID{Num: v}
|
|
return nil
|
|
}
|
|
var v2 string
|
|
if err := json.Unmarshal(data, &v2); err != nil {
|
|
return err
|
|
}
|
|
*id = ID{Str: v2, IsString: true}
|
|
return nil
|
|
}
|
|
|
|
// Conn is a JSON-RPC client/server connection. The JSON-RPC protocol
|
|
// is symmetric, so a Conn runs on both ends of a client-server
|
|
// connection.
|
|
type Conn struct {
|
|
stream ObjectStream
|
|
|
|
h Handler
|
|
|
|
mu sync.Mutex
|
|
shutdown bool
|
|
closing bool
|
|
seq uint64
|
|
pending map[ID]*call
|
|
|
|
sending sync.Mutex
|
|
|
|
disconnect chan struct{}
|
|
|
|
// Set by ConnOpt funcs.
|
|
onRecv func(*Request, *Response)
|
|
onSend func(*Request, *Response)
|
|
}
|
|
|
|
var _ JSONRPC2 = (*Conn)(nil)
|
|
|
|
// ErrClosed indicates that the JSON-RPC connection is closed (or in
|
|
// the process of closing).
|
|
var ErrClosed = errors.New("jsonrpc2: connection is closed")
|
|
|
|
// NewConn creates a new JSON-RPC client/server connection using the
|
|
// given ReadWriteCloser (typically a TCP connection or stdio). The
|
|
// JSON-RPC protocol is symmetric, so a Conn runs on both ends of a
|
|
// client-server connection.
|
|
//
|
|
// NewClient consumes conn, so you should call Close on the returned
|
|
// client not on the given conn.
|
|
func NewConn(ctx context.Context, stream ObjectStream, h Handler, opt ...ConnOpt) *Conn {
|
|
c := &Conn{
|
|
stream: stream,
|
|
h: h,
|
|
pending: map[ID]*call{},
|
|
disconnect: make(chan struct{}),
|
|
}
|
|
for _, opt := range opt {
|
|
opt(c)
|
|
}
|
|
go c.readMessages(ctx)
|
|
return c
|
|
}
|
|
|
|
// Close closes the JSON-RPC connection. The connection may not be
|
|
// used after it has been closed.
|
|
func (c *Conn) Close() error {
|
|
c.mu.Lock()
|
|
if c.shutdown || c.closing {
|
|
c.mu.Unlock()
|
|
return ErrClosed
|
|
}
|
|
c.closing = true
|
|
c.mu.Unlock()
|
|
return c.stream.Close()
|
|
}
|
|
|
|
func (c *Conn) send(ctx context.Context, m *anyMessage, wait bool) (cc *call, err error) {
|
|
c.sending.Lock()
|
|
defer c.sending.Unlock()
|
|
|
|
// m.request.ID could be changed, so we store a copy to correctly
|
|
// clean up pending
|
|
var id ID
|
|
|
|
c.mu.Lock()
|
|
if c.shutdown || c.closing {
|
|
c.mu.Unlock()
|
|
return nil, ErrClosed
|
|
}
|
|
|
|
// Store requests so we can later associate them with incoming
|
|
// responses.
|
|
if m.request != nil && wait {
|
|
cc = &call{request: m.request, seq: c.seq, done: make(chan error, 1)}
|
|
if !m.request.ID.IsString && m.request.ID.Num == 0 {
|
|
// unset, use next seq as call ID
|
|
m.request.ID.Num = c.seq
|
|
}
|
|
id = m.request.ID
|
|
c.pending[id] = cc
|
|
c.seq++
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
if c.onSend != nil {
|
|
switch {
|
|
case m.request != nil:
|
|
c.onSend(m.request, nil)
|
|
case m.response != nil:
|
|
c.onSend(nil, m.response)
|
|
}
|
|
}
|
|
|
|
// From here on, if we fail to send this, then we need to remove
|
|
// this from the pending map so we don't block on it or pile up
|
|
// pending entries for unsent messages.
|
|
defer func() {
|
|
if err != nil {
|
|
if cc != nil {
|
|
c.mu.Lock()
|
|
delete(c.pending, id)
|
|
c.mu.Unlock()
|
|
}
|
|
}
|
|
}()
|
|
|
|
if err := c.stream.WriteObject(m); err != nil {
|
|
return nil, err
|
|
}
|
|
return cc, nil
|
|
}
|
|
|
|
// Call initiates a JSON-RPC call using the specified method and
|
|
// params, and waits for the response. If the response is successful,
|
|
// its result is stored in result (a pointer to a value that can be
|
|
// JSON-unmarshaled into); otherwise, a non-nil error is returned.
|
|
func (c *Conn) Call(ctx context.Context, method string, params, result interface{}, opts ...CallOption) error {
|
|
req := &Request{Method: method}
|
|
if err := req.SetParams(params); err != nil {
|
|
return err
|
|
}
|
|
for _, opt := range opts {
|
|
if err := opt.apply(req); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
call, err := c.send(ctx, &anyMessage{request: req}, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
select {
|
|
case err, ok := <-call.done:
|
|
if !ok {
|
|
err = ErrClosed
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if result != nil {
|
|
if call.response.Result == nil {
|
|
call.response.Result = &jsonNull
|
|
}
|
|
// TODO(sqs): error handling
|
|
if err := json.Unmarshal(*call.response.Result, result); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
var jsonNull = json.RawMessage("null")
|
|
|
|
// Notify is like Call, but it returns when the notification request
|
|
// is sent (without waiting for a response, because JSON-RPC
|
|
// notifications do not have responses).
|
|
func (c *Conn) Notify(ctx context.Context, method string, params interface{}, opts ...CallOption) error {
|
|
req := &Request{Method: method, Notif: true}
|
|
if err := req.SetParams(params); err != nil {
|
|
return err
|
|
}
|
|
for _, opt := range opts {
|
|
if err := opt.apply(req); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err := c.send(ctx, &anyMessage{request: req}, false)
|
|
return err
|
|
}
|
|
|
|
// Reply sends a successful response with a result.
|
|
func (c *Conn) Reply(ctx context.Context, id ID, result interface{}) error {
|
|
resp := &Response{ID: id}
|
|
if err := resp.SetResult(result); err != nil {
|
|
return err
|
|
}
|
|
_, err := c.send(ctx, &anyMessage{response: resp}, false)
|
|
return err
|
|
}
|
|
|
|
// ReplyWithError sends a response with an error.
|
|
func (c *Conn) ReplyWithError(ctx context.Context, id ID, respErr *Error) error {
|
|
_, err := c.send(ctx, &anyMessage{response: &Response{ID: id, Error: respErr}}, false)
|
|
return err
|
|
}
|
|
|
|
// SendResponse sends resp to the peer. It is lower level than (*Conn).Reply.
|
|
func (c *Conn) SendResponse(ctx context.Context, resp *Response) error {
|
|
_, err := c.send(ctx, &anyMessage{response: resp}, false)
|
|
return err
|
|
}
|
|
|
|
// DisconnectNotify returns a channel that is closed when the
|
|
// underlying connection is disconnected.
|
|
func (c *Conn) DisconnectNotify() <-chan struct{} {
|
|
return c.disconnect
|
|
}
|
|
|
|
func (c *Conn) readMessages(ctx context.Context) {
|
|
var err error
|
|
for err == nil {
|
|
var m anyMessage
|
|
err = c.stream.ReadObject(&m)
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
switch {
|
|
case m.request != nil:
|
|
if c.onRecv != nil {
|
|
c.onRecv(m.request, nil)
|
|
}
|
|
go c.h.Handle(ctx, c, m.request)
|
|
|
|
case m.response != nil:
|
|
resp := m.response
|
|
if resp != nil {
|
|
id := resp.ID
|
|
c.mu.Lock()
|
|
call := c.pending[id]
|
|
delete(c.pending, id)
|
|
c.mu.Unlock()
|
|
|
|
if call != nil {
|
|
call.response = resp
|
|
}
|
|
|
|
if c.onRecv != nil {
|
|
var req *Request
|
|
|
|
if call != nil {
|
|
req = call.request
|
|
}
|
|
c.onRecv(req, resp)
|
|
}
|
|
|
|
switch {
|
|
case call == nil:
|
|
log.Printf("jsonrpc2: ignoring response #%s with no corresponding request", id)
|
|
|
|
case resp.Error != nil:
|
|
call.done <- resp.Error
|
|
close(call.done)
|
|
|
|
default:
|
|
call.done <- nil
|
|
close(call.done)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
c.sending.Lock()
|
|
c.mu.Lock()
|
|
c.shutdown = true
|
|
closing := c.closing
|
|
if err == io.EOF {
|
|
if closing {
|
|
err = ErrClosed
|
|
} else {
|
|
err = io.ErrUnexpectedEOF
|
|
}
|
|
}
|
|
for _, call := range c.pending {
|
|
call.done <- err
|
|
close(call.done)
|
|
}
|
|
c.mu.Unlock()
|
|
c.sending.Unlock()
|
|
if err != io.ErrUnexpectedEOF && !closing {
|
|
log.Println("jsonrpc2: protocol error:", err)
|
|
}
|
|
close(c.disconnect)
|
|
}
|
|
|
|
// call represents a JSON-RPC call over its entire lifecycle.
|
|
type call struct {
|
|
request *Request
|
|
response *Response
|
|
seq uint64 // the seq of the request
|
|
done chan error
|
|
}
|
|
|
|
// anyMessage represents either a JSON Request or Response.
|
|
type anyMessage struct {
|
|
request *Request
|
|
response *Response
|
|
}
|
|
|
|
func (m anyMessage) MarshalJSON() ([]byte, error) {
|
|
var v interface{}
|
|
switch {
|
|
case m.request != nil && m.response == nil:
|
|
v = m.request
|
|
case m.request == nil && m.response != nil:
|
|
v = m.response
|
|
}
|
|
if v != nil {
|
|
return json.Marshal(v)
|
|
}
|
|
return nil, errors.New("jsonrpc2: message must have exactly one of the request or response fields set")
|
|
}
|
|
|
|
func (m *anyMessage) UnmarshalJSON(data []byte) error {
|
|
// The presence of these fields distinguishes between the 2
|
|
// message types.
|
|
type msg struct {
|
|
ID interface{} `json:"id"`
|
|
Method *string `json:"method"`
|
|
Result anyValueWithExplicitNull `json:"result"`
|
|
Error interface{} `json:"error"`
|
|
}
|
|
|
|
var isRequest, isResponse bool
|
|
checkType := func(m *msg) error {
|
|
mIsRequest := m.Method != nil
|
|
mIsResponse := m.Result.null || m.Result.value != nil || m.Error != nil
|
|
if (!mIsRequest && !mIsResponse) || (mIsRequest && mIsResponse) {
|
|
return errors.New("jsonrpc2: unable to determine message type (request or response)")
|
|
}
|
|
if (mIsRequest && isResponse) || (mIsResponse && isRequest) {
|
|
return errors.New("jsonrpc2: batch message type mismatch (must be all requests or all responses)")
|
|
}
|
|
isRequest = mIsRequest
|
|
isResponse = mIsResponse
|
|
return nil
|
|
}
|
|
|
|
if isArray := len(data) > 0 && data[0] == '['; isArray {
|
|
var msgs []msg
|
|
if err := json.Unmarshal(data, &msgs); err != nil {
|
|
return err
|
|
}
|
|
if len(msgs) == 0 {
|
|
return errors.New("jsonrpc2: invalid empty batch")
|
|
}
|
|
for _, msg := range msgs {
|
|
if err := checkType(&msg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
var msg msg
|
|
if err := json.Unmarshal(data, &msg); err != nil {
|
|
return err
|
|
}
|
|
if err := checkType(&msg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var v interface{}
|
|
switch {
|
|
case isRequest && !isResponse:
|
|
v = &m.request
|
|
case !isRequest && isResponse:
|
|
v = &m.response
|
|
}
|
|
if err := json.Unmarshal(data, v); err != nil {
|
|
return err
|
|
}
|
|
if !isRequest && isResponse && m.response.Error == nil && m.response.Result == nil {
|
|
m.response.Result = &jsonNull
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// anyValueWithExplicitNull is used to distinguish {} from
|
|
// {"result":null} by anyMessage's JSON unmarshaler.
|
|
type anyValueWithExplicitNull struct {
|
|
null bool // JSON "null"
|
|
value interface{}
|
|
}
|
|
|
|
func (v anyValueWithExplicitNull) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(v.value)
|
|
}
|
|
|
|
func (v *anyValueWithExplicitNull) UnmarshalJSON(data []byte) error {
|
|
data = bytes.TrimSpace(data)
|
|
if string(data) == "null" {
|
|
*v = anyValueWithExplicitNull{null: true}
|
|
return nil
|
|
}
|
|
*v = anyValueWithExplicitNull{}
|
|
return json.Unmarshal(data, &v.value)
|
|
}
|
|
|
|
var (
|
|
errInvalidRequestJSON = errors.New("jsonrpc2: request must be either a JSON object or JSON array")
|
|
errInvalidResponseJSON = errors.New("jsonrpc2: response must be either a JSON object or JSON array")
|
|
)
|