github.com/philippseith/signalr@v0.6.3/jsonhubprotocol.go (about)

     1  package signalr
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"reflect"
    10  
    11  	"github.com/go-kit/log"
    12  )
    13  
    14  // jsonHubProtocol is the JSON based SignalR protocol
    15  type jsonHubProtocol struct {
    16  	dbg log.Logger
    17  }
    18  
    19  // Protocol specific messages for correct unmarshaling of arguments or results.
    20  // jsonInvocationMessage is only used in ParseMessages, not in WriteMessage
    21  type jsonInvocationMessage struct {
    22  	Type         int               `json:"type"`
    23  	Target       string            `json:"target"`
    24  	InvocationID string            `json:"invocationId"`
    25  	Arguments    []json.RawMessage `json:"arguments"`
    26  	StreamIds    []string          `json:"streamIds,omitempty"`
    27  }
    28  
    29  type jsonStreamItemMessage struct {
    30  	Type         int             `json:"type"`
    31  	InvocationID string          `json:"invocationId"`
    32  	Item         json.RawMessage `json:"item"`
    33  }
    34  
    35  type jsonCompletionMessage struct {
    36  	Type         int             `json:"type"`
    37  	InvocationID string          `json:"invocationId"`
    38  	Result       json.RawMessage `json:"result,omitempty"`
    39  	Error        string          `json:"error,omitempty"`
    40  }
    41  
    42  type jsonError struct {
    43  	raw string
    44  	err error
    45  }
    46  
    47  func (j *jsonError) Error() string {
    48  	return fmt.Sprintf("%v (source: %v)", j.err, j.raw)
    49  }
    50  
    51  // UnmarshalArgument unmarshals a json.RawMessage depending on the specified value type into value
    52  func (j *jsonHubProtocol) UnmarshalArgument(src interface{}, dst interface{}) error {
    53  	rawSrc, ok := src.(json.RawMessage)
    54  	if !ok {
    55  		return fmt.Errorf("invalid source %#v for UnmarshalArgument", src)
    56  	}
    57  	if err := json.Unmarshal(rawSrc, dst); err != nil {
    58  		return &jsonError{string(rawSrc), err}
    59  	}
    60  	_ = j.dbg.Log(evt, "UnmarshalArgument",
    61  		"argument", string(rawSrc),
    62  		"value", fmt.Sprintf("%v", reflect.ValueOf(dst).Elem()))
    63  	return nil
    64  }
    65  
    66  // ParseMessages reads all messages from the reader and puts the remaining bytes into remainBuf
    67  func (j *jsonHubProtocol) ParseMessages(reader io.Reader, remainBuf *bytes.Buffer) (messages []interface{}, err error) {
    68  	frames, err := readJSONFrames(reader, remainBuf)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	message := hubMessage{}
    73  	messages = make([]interface{}, 0)
    74  	for _, frame := range frames {
    75  		err = json.Unmarshal(frame, &message)
    76  		_ = j.dbg.Log(evt, "read", msg, string(frame))
    77  		if err != nil {
    78  			return nil, &jsonError{string(frame), err}
    79  		}
    80  		typedMessage, err := j.parseMessage(message.Type, frame)
    81  		if err != nil {
    82  			return nil, err
    83  		}
    84  		// No specific type (aka Ping), use hubMessage
    85  		if typedMessage == nil {
    86  			typedMessage = message
    87  		}
    88  		messages = append(messages, typedMessage)
    89  	}
    90  	return messages, nil
    91  }
    92  
    93  func (j *jsonHubProtocol) parseMessage(messageType int, text []byte) (message interface{}, err error) {
    94  	switch messageType {
    95  	case 1, 4:
    96  		jsonInvocation := jsonInvocationMessage{}
    97  		if err = json.Unmarshal(text, &jsonInvocation); err != nil {
    98  			err = &jsonError{string(text), err}
    99  		}
   100  		arguments := make([]interface{}, len(jsonInvocation.Arguments))
   101  		for i, a := range jsonInvocation.Arguments {
   102  			arguments[i] = a
   103  		}
   104  		return invocationMessage{
   105  			Type:         jsonInvocation.Type,
   106  			Target:       jsonInvocation.Target,
   107  			InvocationID: jsonInvocation.InvocationID,
   108  			Arguments:    arguments,
   109  			StreamIds:    jsonInvocation.StreamIds,
   110  		}, err
   111  	case 2:
   112  		jsonStreamItem := jsonStreamItemMessage{}
   113  		if err = json.Unmarshal(text, &jsonStreamItem); err != nil {
   114  			err = &jsonError{string(text), err}
   115  		}
   116  		return streamItemMessage{
   117  			Type:         jsonStreamItem.Type,
   118  			InvocationID: jsonStreamItem.InvocationID,
   119  			Item:         jsonStreamItem.Item,
   120  		}, err
   121  	case 3:
   122  		jsonCompletion := jsonCompletionMessage{}
   123  		if err = json.Unmarshal(text, &jsonCompletion); err != nil {
   124  			err = &jsonError{string(text), err}
   125  		}
   126  		completion := completionMessage{
   127  			Type:         jsonCompletion.Type,
   128  			InvocationID: jsonCompletion.InvocationID,
   129  			Error:        jsonCompletion.Error,
   130  		}
   131  		// Only assign Result when non nil. setting interface{} Result to (json.RawMessage)(nil)
   132  		// will produce a value which can not compared to nil even if it is pointing towards nil!
   133  		// See https://www.calhoun.io/when-nil-isnt-equal-to-nil/ for explanation
   134  		if jsonCompletion.Result != nil {
   135  			completion.Result = jsonCompletion.Result
   136  		}
   137  		return completion, err
   138  	case 5:
   139  		invocation := cancelInvocationMessage{}
   140  		if err = json.Unmarshal(text, &invocation); err != nil {
   141  			err = &jsonError{string(text), err}
   142  		}
   143  		return invocation, err
   144  	case 7:
   145  		cm := closeMessage{}
   146  		if err = json.Unmarshal(text, &cm); err != nil {
   147  			err = &jsonError{string(text), err}
   148  		}
   149  		return cm, err
   150  	default:
   151  		return nil, nil
   152  	}
   153  }
   154  
   155  // readJSONFrames reads all complete frames (delimited by 0x1e) from the reader and puts the remaining bytes into remainBuf
   156  func readJSONFrames(reader io.Reader, remainBuf *bytes.Buffer) ([][]byte, error) {
   157  	p := make([]byte, 1<<15)
   158  	buf := &bytes.Buffer{}
   159  	_, _ = buf.ReadFrom(remainBuf)
   160  	// Try getting data until at least one frame is available
   161  	for {
   162  		n, err := reader.Read(p)
   163  		// Some reader implementations return io.EOF additionally to n=0 if no data could be read
   164  		if err != nil && !errors.Is(err, io.EOF) {
   165  			return nil, err
   166  		}
   167  		if n > 0 {
   168  			_, _ = buf.Write(p[:n])
   169  			frames, err := parseJSONFrames(buf)
   170  			if err != nil {
   171  				return nil, err
   172  			}
   173  			if len(frames) > 0 {
   174  				_, _ = remainBuf.ReadFrom(buf)
   175  				return frames, nil
   176  			}
   177  		}
   178  	}
   179  }
   180  
   181  func parseJSONFrames(buf *bytes.Buffer) ([][]byte, error) {
   182  	frames := make([][]byte, 0)
   183  	for {
   184  		frame, err := buf.ReadBytes(0x1e)
   185  		if errors.Is(err, io.EOF) {
   186  			// Restore incomplete frame in buffer
   187  			_, _ = buf.Write(frame)
   188  			break
   189  		}
   190  		if err != nil {
   191  			return nil, err
   192  		}
   193  		frames = append(frames, frame[:len(frame)-1])
   194  	}
   195  	return frames, nil
   196  }
   197  
   198  // WriteMessage writes a message as JSON to the specified writer
   199  func (j *jsonHubProtocol) WriteMessage(message interface{}, writer io.Writer) error {
   200  	var b []byte
   201  	var err error
   202  	if marshaler, ok := message.(json.Marshaler); ok {
   203  		b, err = marshaler.MarshalJSON()
   204  	} else {
   205  		b, err = json.Marshal(message)
   206  	}
   207  	if err != nil {
   208  		return err
   209  	}
   210  	b = append(b, 0x1e)
   211  	_ = j.dbg.Log(evt, "write", msg, string(b))
   212  	_, err = writer.Write(b)
   213  	return err
   214  }
   215  
   216  func (j *jsonHubProtocol) transferMode() TransferMode {
   217  	return TextTransferMode
   218  }
   219  
   220  func (j *jsonHubProtocol) setDebugLogger(dbg StructuredLogger) {
   221  	j.dbg = log.WithPrefix(dbg, "ts", log.DefaultTimestampUTC, "protocol", "JSON")
   222  }