github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/protocol/v2/reply.go (about)

     1  // Copyright (c) 2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package v2
     6  
     7  import (
     8  	"encoding/json"
     9  	"fmt"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/choria-io/go-choria/protocol"
    15  )
    16  
    17  type Reply struct {
    18  	// The protocol version for this transport `io.choria.protocol.v2.reply` / protocol.ReplyV2
    19  	Protocol protocol.ProtocolVersion `json:"protocol"`
    20  	// The arbitrary data contained in the reply - like a RPC reply
    21  	MessageBody []byte `json:"message"`
    22  	// The ID of the request this reply relates to
    23  	Request string `json:"request"`
    24  	// The host sending the reply
    25  	Sender string `json:"sender"`
    26  	// The agent the reply originates from
    27  	SendingAgent string `json:"agent"`
    28  	// The unix nano time the request was created
    29  	TimeStamp int64 `json:"time"`
    30  
    31  	seenBy     [][3]string
    32  	federation *FederationTransportHeader
    33  
    34  	mu sync.Mutex
    35  }
    36  
    37  // NewReply creates a io.choria.protocol.v2.request based on a previous Request
    38  func NewReply(request protocol.Request, sender string) (protocol.Reply, error) {
    39  	if request.Version() != protocol.RequestV2 {
    40  		return nil, fmt.Errorf("cannot create a version 2 Reply from a %s request", request.Version())
    41  	}
    42  
    43  	rep := &Reply{
    44  		Protocol:     protocol.ReplyV2,
    45  		Request:      request.RequestID(),
    46  		Sender:       sender,
    47  		SendingAgent: request.Agent(),
    48  		TimeStamp:    time.Now().UnixNano(),
    49  	}
    50  
    51  	protocol.CopyFederationData(request, rep)
    52  
    53  	j, err := request.JSON()
    54  	if err != nil {
    55  		return nil, fmt.Errorf("could not turn Request %s into a JSON document: %s", request.RequestID(), err)
    56  	}
    57  
    58  	rep.SetMessage(j)
    59  
    60  	return rep, nil
    61  }
    62  
    63  // NewReplyFromSecureReply create a choria:reply:1 based on the data contained in a SecureReply
    64  func NewReplyFromSecureReply(sr protocol.SecureReply) (protocol.Reply, error) {
    65  	if sr.Version() != protocol.SecureReplyV2 {
    66  		return nil, fmt.Errorf("cannot create a version 2 Reply from a %s SecureReply", sr.Version())
    67  	}
    68  
    69  	rep := &Reply{
    70  		Protocol: protocol.ReplyV2,
    71  	}
    72  
    73  	err := rep.IsValidJSON(sr.Message())
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	err = json.Unmarshal(sr.Message(), rep)
    79  	if err != nil {
    80  		return nil, fmt.Errorf("could not parse JSON data from Secure Reply: %s", err)
    81  	}
    82  
    83  	return rep, nil
    84  }
    85  
    86  // RecordNetworkHop appends a hop onto the list of those who processed this message
    87  func (r *Reply) RecordNetworkHop(in string, processor string, out string) {
    88  	r.mu.Lock()
    89  	defer r.mu.Unlock()
    90  
    91  	r.seenBy = append(r.seenBy, [3]string{in, processor, out})
    92  }
    93  
    94  // NetworkHops returns a list of tuples this messaged traveled through
    95  func (r *Reply) NetworkHops() [][3]string {
    96  	r.mu.Lock()
    97  	defer r.mu.Unlock()
    98  
    99  	return r.seenBy
   100  }
   101  
   102  // SetMessage sets the data to be stored in the Reply
   103  func (r *Reply) SetMessage(message []byte) {
   104  	r.mu.Lock()
   105  	defer r.mu.Unlock()
   106  
   107  	r.MessageBody = message
   108  }
   109  
   110  // Message retrieves the JSON encoded message set using SetMessage
   111  func (r *Reply) Message() (msg []byte) {
   112  	r.mu.Lock()
   113  	defer r.mu.Unlock()
   114  
   115  	return r.MessageBody
   116  }
   117  
   118  // RequestID retrieves the unique request id
   119  func (r *Reply) RequestID() string {
   120  	r.mu.Lock()
   121  	defer r.mu.Unlock()
   122  
   123  	return r.Request
   124  }
   125  
   126  // SenderID retrieves the identity of the sending node
   127  func (r *Reply) SenderID() string {
   128  	r.mu.Lock()
   129  	defer r.mu.Unlock()
   130  
   131  	return r.Sender
   132  }
   133  
   134  // Agent retrieves the agent name that sent this reply
   135  func (r *Reply) Agent() string {
   136  	r.mu.Lock()
   137  	defer r.mu.Unlock()
   138  
   139  	return r.SendingAgent
   140  }
   141  
   142  // Time retrieves the time stamp that this message was made
   143  func (r *Reply) Time() time.Time {
   144  	r.mu.Lock()
   145  	defer r.mu.Unlock()
   146  
   147  	return time.Unix(0, r.TimeStamp)
   148  }
   149  
   150  // JSON creates a JSON encoded reply
   151  func (r *Reply) JSON() ([]byte, error) {
   152  	r.mu.Lock()
   153  	defer r.mu.Unlock()
   154  
   155  	j, err := json.Marshal(r)
   156  	if err != nil {
   157  		protocolErrorCtr.Inc()
   158  		return nil, fmt.Errorf("could not JSON Marshal: %s", err)
   159  	}
   160  
   161  	err = r.isValidJSONUnlocked(j)
   162  	if err != nil {
   163  		return nil, fmt.Errorf("serialized JSON produced from the Reply does not pass validation: %s", err)
   164  	}
   165  
   166  	return j, nil
   167  }
   168  
   169  // Version retrieves the protocol version for this message
   170  func (r *Reply) Version() protocol.ProtocolVersion {
   171  	r.mu.Lock()
   172  	defer r.mu.Unlock()
   173  
   174  	return r.Protocol
   175  }
   176  
   177  func (r *Reply) isValidJSONUnlocked(data []byte) error {
   178  	// replies are usually not validates as there are too many
   179  	if !protocol.ClientStrictValidation {
   180  		return nil
   181  	}
   182  
   183  	_, errors, err := schemaValidate(protocol.ReplyV2, data)
   184  	if err != nil {
   185  		return fmt.Errorf("could not validate Reply JSON data: %s", err)
   186  	}
   187  
   188  	if len(errors) != 0 {
   189  		return fmt.Errorf("%w: %s", ErrInvalidJSON, strings.Join(errors, ", "))
   190  	}
   191  
   192  	return nil
   193  }
   194  
   195  // IsValidJSON validates the given JSON data against the schema
   196  func (r *Reply) IsValidJSON(data []byte) (err error) {
   197  	r.mu.Lock()
   198  	defer r.mu.Unlock()
   199  
   200  	return r.isValidJSONUnlocked(data)
   201  }
   202  
   203  // FederationTargets retrieves the list of targets this message is destined for
   204  func (r *Reply) FederationTargets() (targets []string, federated bool) {
   205  	r.mu.Lock()
   206  	defer r.mu.Unlock()
   207  
   208  	if r.federation == nil {
   209  		return nil, false
   210  	}
   211  
   212  	return r.federation.Targets, true
   213  }
   214  
   215  // FederationReplyTo retrieves the reply to string set by the federation broker
   216  func (r *Reply) FederationReplyTo() (replyto string, federated bool) {
   217  	r.mu.Lock()
   218  	defer r.mu.Unlock()
   219  
   220  	if r.federation == nil {
   221  		return "", false
   222  	}
   223  
   224  	return r.federation.ReplyTo, true
   225  }
   226  
   227  // FederationRequestID retrieves the federation specific requestid
   228  func (r *Reply) FederationRequestID() (id string, federated bool) {
   229  	r.mu.Lock()
   230  	defer r.mu.Unlock()
   231  
   232  	if r.federation == nil {
   233  		return "", false
   234  	}
   235  
   236  	return r.federation.RequestID, true
   237  }
   238  
   239  // SetFederationTargets sets the list of hosts this message should go to.
   240  //
   241  // Federation brokers will duplicate the message and send one for each target
   242  func (r *Reply) SetFederationTargets(targets []string) {
   243  	r.mu.Lock()
   244  	defer r.mu.Unlock()
   245  
   246  	if r.federation == nil {
   247  		r.federation = &FederationTransportHeader{}
   248  	}
   249  
   250  	r.federation.Targets = targets
   251  }
   252  
   253  // SetFederationReplyTo stores the original reply-to destination in the federation headers
   254  func (r *Reply) SetFederationReplyTo(reply string) {
   255  	r.mu.Lock()
   256  	defer r.mu.Unlock()
   257  
   258  	if r.federation == nil {
   259  		r.federation = &FederationTransportHeader{}
   260  	}
   261  
   262  	r.federation.ReplyTo = reply
   263  }
   264  
   265  // SetFederationRequestID sets the request ID for federation purposes
   266  func (r *Reply) SetFederationRequestID(id string) {
   267  	r.mu.Lock()
   268  	defer r.mu.Unlock()
   269  
   270  	if r.federation == nil {
   271  		r.federation = &FederationTransportHeader{}
   272  	}
   273  
   274  	r.federation.RequestID = id
   275  }
   276  
   277  // IsFederated determines if this message is federated
   278  func (r *Reply) IsFederated() bool {
   279  	r.mu.Lock()
   280  	defer r.mu.Unlock()
   281  
   282  	return r.federation != nil
   283  }
   284  
   285  // SetUnfederated removes any federation information from the message
   286  func (r *Reply) SetUnfederated() {
   287  	r.mu.Lock()
   288  	defer r.mu.Unlock()
   289  
   290  	r.federation = nil
   291  }