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 }